10 PRINT"LET'S READ BASIC COMPUTER GAMES":GOTO 10
Once upon a time - specifically, in the mid-60s - John G. Kemeny and Thomas E. Kurtz decided that they wanted to get students more interested in computers, even the ones that weren't in math and science programs. To that end, they developed two programs. One of them was the Dartmouth Time Sharing System, an operating system that allowed more than one person to use the minicomputers of the time simultaneously. (Minicomputers were essentially smaller, cheaper mainframes, the size of a large refrigerator rather than the size of a room.) People would sit in front of a teleprinter, a device like a typewriter that communicated with the minicomputer, and type on it. What they typed would be both literally typed, on paper, and sent to this computer, which would then devote a fraction of its processing power - the other fractions being devoted to people at other teleprinters - to processing this information. It could then send back additional text for the teleprinter to print.
The other program they developed was BASIC, Beginners' All-purpose Symbolic Instruction Code. With this, anyone could sit down at the teleprinter, create a simple program using relatively easy-to-understand commands, and see the results immediately instead of having to submit their program to be processed in a large batch. This got a lot of people into programming who wouldn't have otherwise bothered. They made BASIC available free of charge, and various people customized it to add desired features or handle the idiosyncracies of the machine they were running it on.
Needless to say, one of the first things people wanted to do with computers was play games on them. Many people wrote games in BASIC, and these games were copied far and wide. Some years later, in 1973, David H. Ahl collected a number of these games into the book 101 BASIC Computer Games, which used the BASIC dialect used by Digital's minicomputers. The book eventually sold 10,000 copies, which Ahl said “was far more books than there were computers around, so people were buying three, four, five of them for each computer.”
Even as he did, though, the first microcomputers, computers that you could actually own and have in your house, started to come out. Most of them wound up having some form of BASIC pre-installed; it was a language powerful enough to actually do things with yet small enough to fit into the limited memory of the systems. In response, Ahl republished the book in 1974, now called simply BASIC Computer Games and with the programs (slightly) adapted to run on these microcomputers. (The versions of BASIC these machines came with were further customized to the machines' capabilities, so the BASIC of the books was kept quite generic, and two pages of info on porting the programs were added.)
The continuing popularity of BASIC led Ahl to later publish three more books of computer games: More BASIC Computer Games (1979), Big Computer Games (1984), and BASIC Computer Adventures (1984). The first book was even re-released in 2010, adapted to Microsoft Small Basic.
BASIC is still in use today, even with the rise of more elegant or powerful programming languages. It's evolved significantly, taking features from newer languages and making them its own, but the older varieties persist in retro equipment, emulators, and programs written to bring back those good old days.
But let's wind the clock back a bit. Back in 1973, when David Ahl was publishing his first book of games, I entered the picture, so to speak. My interest in computers appeared early, though not so early that I actually got to use a teleprinter as intended (though I did get to type my name into one, keeping the little strip of paper tape that came out for some time afterward). My first computer was a TRS-80, Model I, Level I, though my parents shelled out for the extra hardware that gave it upper and lower case characters. And I eagerly learned the secrets of BASIC, mapping out graphics in the TRS-80's strange character set while I waited for the tape drive to laboriously load a game.
From there, I went on to own a Commodore 64, followed by a Commodore 128, an Amiga 500, and an Amiga 1200. Sadly, by the time I got the 1200, I was starting to see the writing on the wall, and my next computer, and all those thereafter, were IBM-compatibles. But throughout the whole journey, I kept my hand in the BASIC well - and part of that was reading and re-reading the library's copies of BASIC Computer Games and More BASIC Computer Games. (Dewey Decimal number 794.8, in case you're curious - and yes, I did have the first three digits from memory.)
So in celebration of nothing in particular, let's dig those books out (in this case, out of Atari Archives, but you can find them elsewhere). We'll go through the programs one by one, see what they do and how they do it, and try to come up with ways to make them better, or at least more interesting. Some of them may even be interesting enough to actually play for a bit!
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"
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
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"
Code: Select all
100 N=100
110 Q=100
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"
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
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
Code: Select all
280 IF A<2 THEN 270
290 IF A>14 THEN 270
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
Code: Select all
330 IF A>=B THEN 270
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"
(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
Code: Select all
660 INPUT"WHAT IS YOUR BET";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
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
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
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
Code: Select all
210 Q=Q+N
220 GOTO 120
Code: Select all
970 PRINT"SORRY, YOU LOSE"
980 IF N<Q THEN 240
Code: Select all
240 Q=Q-N
250 GOTO 120
Code: Select all
990 PRINT
1000 PRINT
1010 PRINT"SORRY, FRIEND BUT YOU BLEW YOUR WAD"
1020 INPUT"TRY AGAIN (YES OR NO)";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"
Code: Select all
1050 END
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?
- Site Admin
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)
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.JamieTheD wrote: ↑Sun Jun 28, 2020 7:29 pmErrrr... My math may be wrong,Code: Select all
24 (00011000) 64 (00111100) 64 (00111100) 126 (01111110) 126 (01111110) 92 (01011010) 24 (00011000) 64 (00111100)
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)
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!
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.)
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.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 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:
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).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.
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
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
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.
I'll cover his coverage, and my response, next time! In the meantime, can you figure out how this code works?
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:
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.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
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.
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
160 Q=0:Z=0:X=INT(RND(1)*H+1)
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
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 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.Code: Select all
Repeat with I running from 1 to H If I=X PRINT ".--"; Otherwise PRINT ". "; PRINT "." and end the line
(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.)
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.Code: Select all
195 C=1:W(X,1)=C:C=C+1 200 R=X:S=1:GOTO 260
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: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.
- 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.
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: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.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
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?
This code has a GOTO on every line but 1, but it's actually well-structured!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
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.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
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: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
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
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.Code: Select all
Loop forever: If NeedScan is true: (lines 210-250) If DoFirstBit is true: (lines 260-520) (rest of loop)
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:
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.Code: Select all
310 X=INT(RND(1)*3+1) 320 ON X GOTO 790,820,860
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.
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.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
In summary, this block of code says "Dig West."
Exactly equivalent code, but now we dig north.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
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.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
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.
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.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 one...
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.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
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:
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
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 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.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)
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.
- 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 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:
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.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
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?
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.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
After sorting out how it worked, I created an equivalent but vastly shorter version of it, and then made some additional modest improvements. This post has gone on far too long as it is, though, so I will save that for another post.
ManxomeBromide 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:The improvements and modifications are sixfold: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
- It now correctly rejects input that specifies zero or negative dimensions.
- The 70-ish lines of code for choosing a direction (lines 260-780) are now more like seven. I create an expandable list, fill it with between 0 and 4 elements, and choose a result from the valid elements. BASIC doesn't have expandable lists as a data type, of course, but I'm not about to let that stop me.
- The digging logic has also been simplified, merging "getting stuck" into the actual logic of executing a chosen direction.
- The random number generator is re-seeded on each run to make sure it won't keep producing the same maze each time.
- I removed the blank lines from after the part where you choose the dimensions, mainly so that a 10-row maze can keep the dimensions visible on a 25-line screen.
- The printing of the maze is slightly more sophisticated, making use of the box-drawing characters in the IBM BIOS codepage that we now know as Code Page 437. The box-drawing characters aren't quite up to the level I'd want them to be at to get a properly proportioned set of maze passages, and even if they were the drawing code would be quite a bit harder to manage, but even if I keep the logic mostly intact I can still get a perfectly acceptable, if spiky, outcome.
A Challenge
There's still a bug lurking in this code somewhere, and it's visible in the screenshot. Can you find it?
Commodore Conversion
The Commodore 8-bits do not have the same graphics characters as the PC, but they do share some of them even when they are in different places. This program is pretty trivially portable to the 40-column Commodore computers (PET 4016, C64, C128, C16, C/+4) with only a few changes:
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.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
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:
Long INPUT prompts seem to confuse the VIC-20 BASIC for some reason.Code: Select all
40 INPUT "MAZE W/H",XM,YM
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.
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
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"
Code: Select all
110 N=VAL(A$(0))
Code: Select all
120 REM MAIN CONTROL SECTION
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
Code: Select all
160 K=1
170 GOSUB 390
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
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
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 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
Code: Select all
510 K=VAL(MID$(Q$,X+2,Y-X-2))
520 RETURN
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
Code: Select all
190 IF LEFT$(A$(K),2)="\Q" THEN 170
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
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
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
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
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?
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
(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!
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 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
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
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
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
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
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
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
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
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
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
Code: Select all
900 FOR I=0 TO N-1:PRINTB(I):NEXT I
999 END
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
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
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.
- Site Admin
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.FredMSloniker wrote: ↑Wed Jul 01, 2020 10:59 pmThis 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!
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.
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.
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?ManxomeBromide wrote: ↑Thu Jul 02, 2020 3:12 pmEDIT: 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.
- Site Admin
> 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.
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$
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
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
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
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
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
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
And that's all the time I'm prepared to spend on this program. Let's squeeze a second one in.
Obviously the utility of this program was somewhat limited even back in the day, but hey, who doesn't like printing huge obnoxious banners? Let's get started with the analysis.
Code: 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$
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
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
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
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
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);
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)
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
Code: Select all
600 NEXT B
620 PRINT
630 NEXT T1
700 NEXT U
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
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.
(On the page with the 'Awari' code.)
(On a page of its own.)
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
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)
Code: Select all
IF (this thing is not true) THEN (go to label)
(do stuff): GOTO (somewhere else)
(label)
(do other stuff)
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
It's easier to understand, but it's treacherous. This burned me in AMAZING a bit.FredMSloniker wrote: ↑Fri Jul 03, 2020 7:24 pmSee how much simpler the code is when we take advantage of the natural flow? It's not a huge savings in code length -
- 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!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
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?"
(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.)
Code: Select all
IF A=3 THEN X=X+2 ELSE X=X+5
Code: Select all
X=X+5+(A=3)*3
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.
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.
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:FredMSloniker wrote: ↑Sat Jul 04, 2020 7:12 pmYou'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).
- 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.)
- Site Admin
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
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.
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: 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.
- 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.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.
Code: Select all
1180 INPUT X,Y
...
1230 R=7-Y
1240 C=X
2150 IF F(R,C)>0 THEN 1290
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
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.
I've actually gotten RISC OS 5 running on my Raspberry Pi 3, and it too runs BBC BASIC like a champ:JamieTheD wrote: ↑Mon Jul 06, 2020 3:23 pmThere'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'm guessing that this is one of the "weird text adventure duo" there
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.
- Site Admin
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)ManxomeBromide wrote: ↑Tue Jul 07, 2020 3:13 am
I'm guessing that this is one of the "weird text adventure duo" there
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.
This is how I imagine FredMSloniker typing in these programs:
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."
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.ManxomeBromide wrote: ↑Tue Jul 07, 2020 6:55 pmI 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."
Let's take a brief look at a long program.
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?
You know what? Challenge accepted.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.
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
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
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
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
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
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
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!