|
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!
|
# ? Jun 22, 2020 05:35 |
|
|
# ? May 3, 2024 05:17 |
|
Before I get started analyzing this program in particular, I'm going to discuss some features of BASIC in general. Be warned, however, that BASIC existed in many shapes and forms even back then, so I'm only describing a sort of generic BASIC, influenced by the programs in this book and by my own experience with Commodore BASIC V2 and QuickBASIC. Stick "usually" in wherever needed. Interpreters versus compilers Many languages are compiled languages. You write code in one or more text documents, then run a program that takes that code and turns it into another program that you can run. BASIC, however, is an interpreted language; the BASIC program takes the written code and executes it immediately. This is convenient for the purposes of debugging and testing, but it means that you must have access to the BASIC interpreter to run BASIC programs. Some modern BASIC implementations compile your code, and some act as both interpreters and compilers. Lines and Line Numbers A line of code contains one or more statements, separated by colons. It usually doesn't matter if code is on one line or multiple lines, but there's a limit on how many characters can be on a single line (and what that limit is depends heavily on which version of BASIC you're using), and the IF - THEN command (more on that later) cares about what else is on that line in particular. Every line of code in a BASIC program has a line number, which appears at the start of the line. These numbers must be integers in the range 1-65535, but other than that, there are no restrictions, and it's commonplace to leave gaps between numbers in case you decide to insert a line or twelve later. Barring other factors, the BASIC interpreter goes through the code in ascending line number order, then stops. Line numbers are also used for flow control; more on that when it becomes relevant. Note that, when entering a BASIC program, if you enter a line number that's equal to an existing line number, the line you enter replaces the old line; if you enter a line number before an existing line number, the line you enter is inserted into the program at that point. Feeding a text file to the interpreter wasn't generally supported, but if you try it with a more modern interpreter, it's likely to complain if the line numbers aren't in order, and if it doesn't it'll probably reorder the lines automatically. Needless to say, line numbers are among the first things ditched by more modern BASICs. Variables The most common type of variable in BASIC is the floating-point number. Variable names are one or two characters, with the first character being a letter and the second, if it exists, being a letter or digit. BASIC is not a case-sensitive language, and in the earliest examples didn't use lower case characters at all. Variables can also contain strings of zero or more characters. (How many characters maximum is platform-dependent, but expect no more than 255, and possibly less.) A string variable has the character $ immediately after its name. Literal strings in code are enclosed by quote marks. I'll discuss putting a quote mark in a string when it becomes relevant. Some variants of BASIC have additional types of variables. For instance, QuickBASIC offers the suffixes % (integer in the range -32,768 to 32,767), & (long integer in the range -2,147,483,648 to 2,147,483,647), and # (double-precision floating point number, using eight bytes of storage instead of four). You can also have a variable array. A(3,4) is a specific element of a two-dimensional array of floating point numbers. Arrays can be of any type that variables are, but cannot store mixed types of variables. Arrays are typically zero-indexed (for instance, A(0,4) is a valid element), but many BASIC programs don't take advantage of that. Variables do not, in general, have to be defined before they can be used. A numeric variable's value defaults to 0, while a string variable's value defaults to "" (the empty string). Arrays are a partial exception to this rule. An array will be automatically defined as having a single dimension with elements 0 to 10 the first time it's used; if you want a larger array (or a smaller one, if you're saving memory) or one with more dimensions, you need to define it first, using the DEF command. (For instance, you might use DEF CH(7,7) to create a two-dimensional array suitable for storing information about a checkerboard.) That said, whether explicitly or implicitly defined, an array's elements have the same default value as variables of that type. All variables have global scope. That means that they can be read, and changed, anywhere within the program. Last, but not least, A, A$, A(), and A$() are all unique variables. (You can't have more than one array of the same name and type, though, even if their numbers of dimensions are different.) This can be a source of confusion both in reading code and in porting it to more modern versions of BASIC, which won't let you do that. I Told You That Story So I Could Tell You This One 10 PRINT TAB(26);"ACEY DUCEY CARD GAME" 20 PRINT TAB(15);"CREATIVE COMPUTING MORRISTOWN, NEW JERSEY" A lot of BASIC programs start with line number 10 and advance by tens. You'll see why in just a couple of lines. For now, though, let's focus on this one. The command PRINT sends output to the screen, or in this case the teleprinter. It can take zero or more arguments separated by commas or semicolons; if a comma is used, it advances to the next tab stop, while if a semicolon is used, it doesn't. It can also have a semicolon at the end of the arguments; if it does not, the cursor (or printhead) moves to the start of the next line. The function TAB, when called by PRINT, advances the cursor to the given column. Note that this does not mean 'advance this many spaces' or, on a screen, 'print a number of spaces'; it doesn't erase text. It's used in this line and the next to center the title of the game, presuming the screen is 80 columns wide. 21 PRINT 22 PRINT 23 PRINT "Oh hey," said the programmer, "this would look nicer if there were a couple of blank lines here. It's a good thing I have nine whole line numbers unused to put these lines in." (Note that the programmer didn't use any multi-command lines; they could have used PRINT:PRINT:PRINT to get the same effect. Perhaps their version of BASIC didn't support that.) 30 PRINT"ACEY-DUCEY IS PLAYED IN THE FOLLOWING MANNER " 40 PRINT"THE DEALER (COMPUTER) DEALS TWO CARDS FACE UP" 50 PRINT"YOU HAVE AN OPTION TO BET OR NOT BET DEPENDING" 60 PRINT"ON WHETHER OR NOR YOU FEEL THE CARD WILL HAVE" 70 PRINT"A VALUE BETWEEN THE FIRST TWO." 80 PRINT"IF YOU DO NOT WANT TO BET, INPUT A 0" Nothing much to say here, except that I see a few typos in the instructions. 100 N=100 110 Q=100 Our first two variables! I'm so proud. Some varieties of BASIC provide the LET command to explicitly assign a value to a variable, and I'm sure some varieties of BASIC require it, but none I'm familiar with. As we'll see later, Q is the amount of money we have at any given point, while N will be used for the amount of our bet. (Why was it assigned a value here? I'm not sure. I'll bring it up later.) The = symbol is used both for assigning a value and for testing for equality. Which it does is determined by context. The command A=B=2 would assign a value to A depending on whether or not B is 2, but it would not change B. 120 PRINT"YOU NOW HAVE ";Q;" DOLLARS" Looking at this command, you might think there should be only one space on either side of the value of Q, but if you look at the sample output, you'll see two. The rules for spacing when printing a number are one of the things that change most from one interpreter to another. Will they print a space before the number? Will they only do so if the number is positive (so 1 takes the same room as -1)? Will there be a space after the number? The programmer made an incorrect assumption here. There are some hijinks you can get up to if your flavor of BASIC forces spaces you don't want; I'll cover them if and when they become relevant. 130 PRINT 140 GOTO 260 I mentioned earlier that line numbers are also used for flow control. The GOTO command immediately jumps command execution to the line number given. (If there are commands on the same line after the GOTO, they aren't actually run.) Liberal use of the GOTO can lead to what's known as 'spaghetti code', and is ideally avoided, but if you run out of line numbers in a certain section, it's so tempting to jump somewhere fresh to finish what you're doing... Note that whether you can put an equation instead of a number and jump to the line number the equation equals is, again, dependent on the interpreter, as is what happens if you try to jump to a line number that doesn't exist. We'll skip over lines 210-250, much as the interpreter is, for now. 260 PRINT"HERE ARE YOUR NEXT TWO CARDS " 270 A=INT(14*RND(1))+2 You can't have a game about drawing cards without randomness! The RND function works differently on different interpreters depending on the number you feed it, but in this particular flavor of BASIC, feeding it a positive number causes it to return a theoretically random number from 0 to not quite 1. Multiplying it by 14 gives a number between 0 and not quite 14; the INT function returns that number rounded down to the next integer, so we have 0-13. Last, but not least, we add 2, so we now have 2-15. 280 IF A<2 THEN 270 290 IF A>14 THEN 270 Which is weird, because if we do get a 15, we immediately throw it away. The IF - THEN command checks if the equation between IF and THEN is true; if it is, it executes the remaining commands on the line, but if it isn't, it skips them. If you just give a number, the command GOTO is assumed. Note that, if the equation in line 280 is ever true, something weird is going on. 300 B=INT(14*RND(1))+2 310 IF B<2 THEN 300 320 IF B>14 THEN 300 We pick a second random number, with the same weirdness about its bounds, here. Note the repeated code, a common source of errors in BASIC. Many flavors of BASIC have some way to avoid this, but it's often limited. 330 IF A>=B THEN 270 Here's a bit of sloppy code. If the first card we draw is higher than or equal to the second, we throw both cards away and start over. What I'd do is start over if the cards are equal, but do IF A>B THEN ZZ=A:A=B:B=ZZ to swap the cards. (Variables starting with Z were always my favorite for 'a value I just need for a little bit', and ZZ was the most common of those.) Actually, I'd do quite a bit more regarding the implementation of drawing cards in this program, but I'll get to that some other time. 350 IF A<11 THEN 400 360 IF A=11 THEN 420 370 IF A=12 THEN 440 380 IF A=13 THEN 460 390 IF A=14 THEN 480 400 PRINT A 410 GOTO 500 420 PRINT"JACK" 430 GOTO 500 440 PRINT"QUEEN" 450 GOTO 500 460 PRINT"KING" 470 GOTO 500 480 PRINT"ACE" All of this code is just to say that if A is less than 11, print the number A; otherwise, print the name of the face card (or the ace, which is high in this game). The version of BASIC the programmer is using must be very limited to require these gymnastics; I can think of a number of ways to improve on this. (In the sample printout, you can see that there's a space in front of the numbers, but not in front of the named cards. There are a few ways this could be handled.) 500 IF B<11 THEN 550 510 IF B=11 THEN 570 520 IF B=12 THEN 590 530 IF B=13 THEN 610 540 IF B=14 THEN 630 550 PRINT B 560 GOTO 650 570 PRINT"JACK" 580 GOTO 650 590 PRINT"QUEEN" 600 GOTO 650 610 PRINT"KING" 620 GOTO 650 630 PRINT"ACE" 640 PRINT 650 PRINT And by repeating the code, we introduce a bug. Note that, if the second card is an ace, we print two lines after it, but if it's any other card, we only print one. 660 INPUT"WHAT IS YOUR BET";N The INPUT command works just like the PRINT command, except its last argument is a variable into which player input is put. In this case, it prints WHAT IS YOUR BET. If a comma followed this, it would advance to the next tab, but instead it leaves the cursor in place. Then (in this flavor of BASIC, at least) it prints a question mark and a space before allowing the user to enter something, which is stored in N. Note that we don't check to see whether what the player entered is a number. In fact, if it's not, the interpreter is well within its rights to stop execution. (In Commodore BASIC V2, it displays the error ?REDO FROM START, then displays the prompt again.) This is only the first place where the programmer assumes the player isn't making a mistake. Note also that it is legal to have the command INPUT A. In this case, it would just print a question mark and a space before taking input. Note also also that in at least some flavors of BASIC, entering nothing at all doesn't change the variable's previous value. This may be why N was assigned a value back in line 100. 670 IF N<>0 THEN 680 675 PRINT"CHICKEN!!" 676 PRINT 677 GOTO 260 The wedged-in lines suggest that the taunt was a last-minute addition. Line 670 probably originally read IF N=0 THEN 260. (Since it hasn't come up yet: <> is BASIC's 'is not equal to' operator.) 680 IF N<=Q THEN 730 690 PRINT"SORRY, MY FRIEND BUT YOU BET TOO MUCH" 700 PRINT"YOU HAVE ONLY ";Q;" DOLLARS TO BET" 710 GOTO 650 And this is the last of the game's sanity checks. What do you think has been missed out? The correct answer is 'checking that the player entered a positive integer'. It will happily let us bet -3829.7386 dollars. 730 C=INT(14*RND(1))+2 740 IF C<2 THEN 730 750 IF C>14 THEN 730 760 IF C<11 THEN 810 770 IF C=11 THEN 830 780 IF C=12 THEN 850 790 IF C=13 THEN 870 800 IF C=14 THEN 890 810 PRINT C 820 GOTO 910 830 PRINT"JACK" 840 GOTO 910 850 PRINT"QUEEN" 860 GOTO 910 870 PRINT"KING" 880 GOTO 910 890 PRINT"ACE" 900 PRINT Yet again, we have repeated code, and the out-of-bounds bug and the issue with only printing a blank line after an ace rear their ugly heads. 910 IF C>A THEN 930 920 GOTO 970 930 IF C>=B THEN 970 950 PRINT"YOU WIN!!!" 960 GOTO 210 Each card has a numeric value from 2 to 14, with 11, 12, 13, and 14 being Jack, Queen, King, and ace respectively. We check here if the player has won or not. This is another place where the code is unnecessarily convoluted. If the player wins, we go to line 210... 210 Q=Q+N 220 GOTO 120 ...which applies their winnings and goes to show them. If they lose, however, we go to line 970... 970 PRINT"SORRY, YOU LOSE" 980 IF N<Q THEN 240 ...and unless they've bet their last dollar... 240 Q=Q-N 250 GOTO 120 ...we go there. But what if they have? 990 PRINT 1000 PRINT 1010 PRINT"SORRY, FRIEND BUT YOU BLEW YOUR WAD" 1020 INPUT"TRY AGAIN (YES OR NO)";A$ Note that we can collect input in a string. Note also that A$ is a different variable from A. Also note that there's a bug in line 980. Do you see it? Here's a hint: it's technically not a bug in that line. 1030 IF A$="YES" THEN 110 1040 PRINT"OK HOPE YOU HAD FUN" Finally, note that the player must type YES, exactly, to have another go. Typing Y will not suffice. 1050 END The END command immediately ends program execution. There's no point in having it at the end of the program, but it does have its uses. The final analysis Leaving aside how fun it is to play a gambling game on a teleprinter (and there are several more gambling games to come), this game is coded using the bare basics, pardon the pun, of the BASIC language. Even without using other features, the code could be cleaned up, and we found a few bugs. Bringing in features like arrays and subroutines (which we haven't touched on) would make significant improvements. And the presentation could be fixed up too. So if you were sitting down to this game back in the day, saying to yourself, "I could do better than this," what would you want to change?
|
# ? Jun 22, 2020 05:52 |
|
I haven't tested this but I think this should work.code:
Edit: Fixed some stuff after checking if it actually works or not (on this site). It does now. Tiggum fucked around with this message at 13:47 on Jun 22, 2020 |
# ? Jun 22, 2020 08:29 |
|
And I just realised I should have addedcode:
|
# ? Jun 22, 2020 08:37 |
|
I was thinking, assume this basic interpreter is so simple there's no support for subroutines or functions or whatever, this game is simple enough that you could wing it. Just have the repeated code (e.g. the card printing function) in a block of line numbers at the end like Tiggum had. Then, you need a single extra variable on top of the P variable that Tiggum introduced, one that's the equivalent of a call stack. Doesn't even need to be an array because you'll call every subroutine directly from your main. Just set it to the line number of the "GOTO 1000" + 1 and that should do the trick, as long as you replace the "RETURN" to GOTO whatever you stored in that variable.
|
# ? Jun 22, 2020 12:12 |
|
Carbon dioxide posted:I was thinking, assume this basic interpreter is so simple there's no support for subroutines or functions or whatever, this game is simple enough that you could wing it.
|
# ? Jun 22, 2020 12:23 |
|
Hell is other people's code. I loved the Ahl book and used to go to the library and just read through it. I tried a few of the longer games once we had a computer at home. My instinct any time there's a deck of cards is to dim an array of strings, which could be done here. Gets rid of the whole subroutine issue as well, because you just have PRINT R$(A) or whatever. I feel like for a lot of the programmers working at the time, there's a tendency to avoid using up space unless there was a necessity for it, because they may have been coming from an era where memory was nearly nonexistent. On the other hand. having enough memory for your code is an issue as well.
|
# ? Jun 22, 2020 14:06 |
Keeping the source code compact is pretty valuable in this context, given that you're expected to type it in. Hard to learn to program when the sample is really long, and you have to worry about typos throwing it off.
|
|
# ? Jun 22, 2020 14:15 |
|
Kangra posted:My instinct any time there's a deck of cards is to dim an array of strings, which could be done here. Gets rid of the whole subroutine issue as well, because you just have PRINT R$(A) or whatever.
|
# ? Jun 22, 2020 14:53 |
|
I've heard that there used to be programming shows on the radio. They would literally play a bunch of weird noises but if you recorded that to a tape and then put that in your home computer's tape reader the result would actually be a program or game you could run.Tiggum posted:You'd still need to track the card values separately though to be able to compare them, which strikes me as adding a little bit of unnecessary complication. You COULD use the array indices for that, especially if you use a 2D array where the first dimension is the suits. In a modern language I would honestly just make a card data type that contains a name, suit, and value and just dump one of each in a big list and be done with it. Carbon dioxide fucked around with this message at 15:14 on Jun 22, 2020 |
# ? Jun 22, 2020 15:08 |
|
Carbon dioxide posted:You COULD use the array indices for that, especially if you use a 2D array where the first dimension is the suits. Oh yeah, that is an excellent idea that I'm annoyed I didn't think of.
|
# ? Jun 22, 2020 16:30 |
|
I should have expected some people to already be familiar with BASIC and want to do it themselves rather than just throw out suggestions! Let's see what we've got.Tiggum posted:I haven't tested this but I think this should work. Okay, let's see what you added. Subroutines You're making use of a subroutine! Subroutines are shown in later programs in the book, so let's go ahead and cover them now. The GOSUB command tells the interpreter to go to a different line and start executing the code there. The difference between that and GOTO is, eventually you (should) use the command RETURN, which tells the interpreter to go back. (That means, among other things, that code after a GOSUB on the same line does eventually get executed, while code after a GOTO does not.) That said, subroutines are very limited in the BASIC of the era. The only thing GOSUB gives you is the ability to return from whence you came. Subroutines don't have variables of their own, and the only way you can pass them values or have them return values is to pre-arrange variables set aside for that purpose. You can nest GOSUB commands, but recursively calling a subroutine isn't useful. And because the code of the subroutine isn't special, you have to make sure execution doesn't fall through to them, which is one of the two primary uses of the END command. (The other being stopping the program immediately, of course.) Tiggum's Rewrite Your subroutine takes the value of P (the program copies the value of interest into P before calling the subroutine) and prints the name of a card, followed by a blank line. But what is P? It's a number between 1 and 52, because you've changed the program so it tracks suits as well, even though they're not relevant to the game. Then you're using D() to translate these internal card values into ranks. I'd make use of modulo math myself, but I haven't introduced that, and at any rate your approach works. You've made a mistake, though: in Acey Ducey, aces are high, and your code assumes they're low. I'd change line 160 to FOR X=2 TO 14 to fix this and make adjustments elsewhere accordingly. You've got code in to make sure you draw three different cards and to skip unwinnable scenarios. Cool beans. And you've added code to allow the two first cards to be either high-low or low-high and handle the scoring properly. That's not the approach I suggested before, but it certainly works! You haven't really done anything in the vein of sanity-checking input, but that's fine; I haven't introduced those tools anyway. And you've added a way to quit before you bust. That's a nice quality-of-life feature. I see you tested this in Applesoft BASIC, which means line 310 is correct, since that variant of BASIC doesn't put spaces around numbers. I'm not sure why you've inserted GOTO after each THEN command, though; it may be required in some BASIC variants, but not that one. That said, the Apple ][ has a 40-column screen. How would you adapt the program to take that into account? A tiny nit: you have an extraneous space at the end of line 330. Carbon dioxide posted:I was thinking, assume this basic interpreter is so simple there's no support for subroutines or functions or whatever, this game is simple enough that you could wing it. The only problem with your suggestion is - Tiggum posted:And if it GOTO variable isn't allowed then you can just use IF calledfrom = 1 THEN GOTO resumepoint etc. That's what I meant by "using an incrementing variable and GOTOs". - beaten, both on the nitpick and on the workaround. As for the discussion on storing card names in a string array, that could work! There are a couple of ways to do it. One approach would be DATA statements, which I'll cover when they become useful. The approach I'd take, though, is to run Tiggum's subroutine on every possible card value; instead of printing the strings, it'd store them in the entries of the array. Then the program can just print CD$(5, 1) to (say) print 5 OF HEARTS. Carbon dioxide posted:I've heard that there used to be programming shows on the radio. They would literally play a bunch of weird noises but if you recorded that to a tape and then put that in your home computer's tape reader the result would actually be a program or game you could run. I've seen one better on a Twitch channel that streams old computer-related TV shows. One program had a DIY project, spread out over multiple shows, to build a light sensor that would decode flashes into the sounds that one of the computers of the day used on its tape drive. Once you'd built the sensor, you would then stick it to your TV screen at the appropriate time, and they'd flash a light in that corner of the screen, and you'd record the output of that. There's also more than one vinyl record out there that has a program recorded on a section of track that the needle never goes to on its own. Some crazy Easter eggs of the era, lemme tell you. Anyway. I'd had some plans of my own on how to improve this program, starting with actually implementing a deck of cards (so you don't pull the ace of hearts multiple times until the deck's mostly been gone through). Then I realized I was missing a step, and I looked up the actual rules for Acey Deucey. Not only did I learn that that is the correct spelling of the name, I learned that it's actually a multiplayer game! Wikipedia posted:Before the action, each player must add their ante into the pot. Two cards are then dealt face-up to one player. That player then bets from nothing to the amount that is in the pot at the time whether or not the third card will numerically fall in between the first two. If the third card falls in between the two other cards, the bettor takes the amount he bet out of the pot; if the third card falls outside of the two other cards, the bettor must add what he bet to the pot; and if the third card matches the numerical value of one of the other two cards, the bettor must add to the pot double what they bet. If two cards of the same value come up, e.g. 2,2 the bettor picks if the next card will be higher or lower and bets. If the next card is the same as the last two, i.e. a 2, the bettor must triple their bet. So here's a project for you: how would you rewrite the game so that you're playing against a computer opponent using these rules? If you don't want to write actual code, that's fine; if you do, but you're inexperienced with BASIC, just ask 'how would I' and I'm sure someone will be glad to help you out. I'll put my answer to the question up some time later. e: if making a computer opponent seems too daunting, you can make it a two-player game. If making the game at all seems too daunting, don't worry! I'm not collecting assignments or anything, and we'll move on to the next program in due course. FredMSloniker fucked around with this message at 20:33 on Jun 22, 2020 |
# ? Jun 22, 2020 18:23 |
|
FredMSloniker posted:I'm not sure why you've inserted GOTO after each THEN command, though; it may be required in some BASIC variants, but not that one. FredMSloniker posted:That said, the Apple ][ has a 40-column screen. How would you adapt the program to take that into account?
|
# ? Jun 23, 2020 04:18 |
|
Oh, this should be fun to follow along with, and I like the idea of this being more about reading the code as opposed to trying to actually pretend to enjoy the games. I too grew up with the Ahl books in the library, and thanks to your posts earlier in the retrogaming forum, I wound up nerdsniping myself with the next game in this article. ... good luck explaining the program code as-is. I managed to work out its logic, but my conclusion for improving the program involved burning something like 80% of it completely to the ground.
|
# ? Jun 23, 2020 05:25 |
|
I just wanted to honor the OPs excellent post by both typing in the listing and running it on actual C64 hardware. I could of course have copied the text of the listing over (be sure to convert it to lowercase for the C64 first in that case!) and done it like that, but it wasn't a particularly huge listing, so I typed it in from the posted listing as that was more legible than the magazine-scan. As you can see it works perfectly and there are no typos I can see. Sorry for the poo poo-tastic image of the CRT, obviously looks much better in person. After this, I bet the house and lost... EDIT: So that this is not just content free gush: On the C64, PET, C16 and the likes you can speed up this code a tiny bit by substituting RND(1) for RND(.) - it does the same thing but is faster as all of these machines are slower with integers than they are with floats. This is apparently a result of being severely constrained for ROM space when they implemented BASIC on those. Go figure. VomitOnLino fucked around with this message at 06:10 on Jun 23, 2020 |
# ? Jun 23, 2020 06:06 |
|
So I whacked together a game of Acey Deucey following the rules off of Wikipedia in several hours. I didn't want to bother coding a CPU opponent, so it requires 2-8 players. For whatever reason, I decided to only have one statement per line, which means some of the code is kind of messy. And even though I verified my code in Commodore BASIC, I didn't use any of that language's unique features, like actually having card suits in the character set. So I'm not going to throw the code for the whole thing up unless there's genuine interest (it's 190 lines). That said, I'm rather proud of this subroutine I worked up for handling drawing a card from the deck. See, since we don't need the suits, just the ranks, it's possible to do it fairly compactly.code:
Once that's out of the way, we set FA to a random number in the range [0, TC - 1]. (Variables starting with F are my choice for variables that a subroutine will use, whether for input, output, or internal processing.) We also set FZ to 1. (We used it briefly as a loop variable, but it's going to be our output.) The section from 1500 to 1530 is where the magic happens. IF FA < RC(FZ) at this point, then FA is in the 0-3 range, so we have one of the four twos (why twos? I'll get to that) and jump to 1540. If it isn't, though, we knock RC(FZ) off of FA, add one to FZ, and loop back around. Eventually FA will get low enough that we'll have found the rank of the card we drew. (Instead of shuffling the deck, we're counting down a random number of cards and drawing that.) Line 1530's IF isn't strictly needed, but I put it in when doing some debugging, and it doesn't do any harm to leave it there. Lines 1540 and 1550 remove the card from the deck. (I forgot 1550 when I was first coding, which first caused a crash when FZ went over 13, then, when I added 1530's IF, caused too many aces to start showing up before I realized why.) Then line 1560 adds 1, moving the range to 2-14 (with 14 being an ace high - in the main body of code, when the rules permit an ace to be low, its internal value is 1) before I return from the subroutine. Writing this up, I realize there's a major bug in the code, though. It just about works if you're drawing one card at a time and discarding them immediately, but if you need to draw three cards and there are only two left in the deck, the third one could be a duplicate of either of the first two - and that's not even considering if you were playing a game where the players had hands of cards! What I should do is have a second array for the discard deck, with its own total cards value and rank variable array; I can 'shuffle' it back into the play deck by adding the corresponding array values and card counts together, then setting the discard deck's numbers to 0. But I'll revisit this the next time we look at a card game - and yes, there will be a next time. For now, though, our guest speaker was so eager to get started that he's barged in on the previous lesson, so put your hands together for ManxomeBromide as we move to the next program in the book, Amazing! I'll cover whatever he doesn't cover once I've fully parsed this myself. It looks like that might take a while.
|
# ? Jun 23, 2020 06:24 |
|
I've still got some prep work to do on it, but I'm going to just note before crashing out that there is a functionality-breaking typo in this listing. Line 695 is incorrect. It should be possible to figure out what it should be even without working out the full details of the listing.
|
# ? Jun 23, 2020 09:58 |
|
This BASIC code is hard to read. I'll just wait for you folks to come up with the bit-by-bit explanation if you don't mind.
|
# ? Jun 23, 2020 17:50 |
|
Guest Lecture: Amazing I'm not going to do a line-by-line exegesis of the code the way FredMSloniker has, for three reasons. First, many of the basics have already been explained, at a line-by-line level. Second, this code is extremely repetitive and, as we'll see, also very inefficient of programmer time. Finally, looking at the trees gives us no insight into the forest here. What I will mostly instead be doing is describing how I analyzed the program, expecting to replicate it. There is one new BASIC construct in the code, though, so I will point that one out when we reach it. That said, the first thing I did with the program was type it in while being mindful of what it was I was typing. I would not expect full understanding, but a core skill here is to be able to maintain incomplete truths in your head as you go, trusting that you will be able to make sense of them later. The Prologue The opening is mostly boilerplate, but we're definitely not off to a good start here: code:
Note that even though we're using IF/GOTO here, this is still basically a do-while loop in a language like C or Java. Line 110 then dimensions two arrays named W and V that match the dimensions we've set. This is the first place I have to deviate in my personal copy of the code; I'm using a QBASIC-compatible variant of BASIC for this and that means I can't have a variable and an array be named V. My personal copy gets H and V renamed XM and YM, for X-Max and Y-Max. Lines 120-150 produce a symmetrical spacing around the prompt to match line 30. code:
code:
pre: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.) code:
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:
So let's put this mess behind us momentarily and look at the final part of the program, lines 1010-1073. Printing Out The Maze This is a series of nested, well-structured loops full of equally well-structured conditional printing statments: code:
I'm a little bit cranky about that, to be honest; there's a very noticable delay between the first line of the maze and the rest of it, when you run it. Better, I think, to rely on the fact that the values in W() are ever increasing. We don't need to dump the top row right away, just as we've determined what the first cell is; as we discovered in the main program loop, writes to W() are ever-increasing and so we could identify column I as the entrance simply by seeing if W(I,1) was 1! Lines 1045-1060 print out the southern walls of each cell based on, essentially, whether or not V(I,J) is even or not; it's checking against the specific values of 0 and 2, but we noticed when typing in the main program loop that V never receives a value above 3. Similarly, and moving backwards slightly, the westernmost wall has always exists and is unconditionally printed at the start of each row by line 1011, and lines 1013-1030 conditionally print out an eastern wall depending on whether the value in V(I,J) is less than 2 or not; a value of 0-1 means there is a wall, and a value of 2-3 means there is a passage. This means that we now know what the V() array is for! It's tracking the actual shape of the maze, and each entry is basically two flags. Each cell's value starts at zero. If there is a passage in this cell to the south, add 1. If there is a passage to the east, add 2. This also means there's no need for any special logic to print out the exit the way we needed it for the entrance; it is stored in V() the same way everything else was. Furthermore, alas, we have nowhere else to hide. We must now dive into the maelstrom of the maze generation logic. Deciphering the Main Program Loop The first time we enter the main loop from lines 210-1000, we skip to line 260. What did we miss? code:
pre:Repeat If R is not H, increment R. Otherwise, if S is not V, reset R to 1 and increment S. Otherwise, R and S are both reset to 1. While W(R,S) is zero This code also shows R and S properly in use. R and S were initialized to X and 1. Throughout the program, they are only ever used by checking their values against 0, H, or V (H always for R, and V always for S), or as an index into the W() and V() arrays. Their initial value is (X, 1)—the maze entrance. When written, they are only ever incremented, decremented, or force-set to 1. R and S are specifying a pair of coordinates within the maze grid, serving much the same purpose as a pointer or a reference, just, well, in a language that has neither. If you find some old enough textbooks, you will see this technique given a name: the (R,S) pair is a cursor into V() and W(). And very like a cursor, what this loop is doing is advancing the cursor through the maze as if it were scanning text on a page, looking for the next cell for which W(R,S) is not 0. If it hits the end, it starts over at the top left. This is a routine that causes the cursor to scan for the next maze cell that has already been visited. The next wodge of code to structure is rather large and indigestible: code:
pre: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: code:
The Random Jump Targets Blocks 790-815, 820-850, 860-905, and 910-1000 are all well-structured blocks, and are also the only way out of lines 210-780, except for one line that jumps directly to line 1000. Let's look at them in turn to see what alternatives are being randomly selected. code:
In summary, this block of code says "Dig West." code:
code:
The general logic of the code guarantees that if we're in this code at all, V(R,S) can only be 0 or 1; lines 870-880 could be replaced with 870 C=C+1:V(R,S)=V(R,S)+2 with no harm to the code. (What you really want is to bitwise OR it with 2, but that's out of the scope of our analysis here.) This is the only branch that skips the 260-520 block in the main loop. We don't yet know why. code:
If Q is one... code:
It's not clear yet what the significance of these actions are, but this is also the only branch where C is not incremented and also the only branch where no new cells are dug out; it's simply opening a passage within the current cell and then scanning. We'll keep that in mind. Back to the Main Code We know what the random jumps do. Let's revisit the main loop again, this time up to the few random jumps: code:
pre:If it's OK to go west If it's OK to go north If it's OK to go east Dig in a random direction chosen between (North, East, West) otherwise 330 If we aren't at the bottom then GOTO 340 334 If Z=1 then GOTO 370 338 Q=1:GOTO 350 340 If the cell to our south is visited then goto 370 350 Dig in a random direction chosen between (North, West, South) 370 Dig in a random direction chosen between (North, West)
This does teach us a few things. Remember how Z was set to 1 only when we dug south while Q was 1? Q is only set to 1 if we're on the bottom row of the maze and Z is still 0! We've finally figured out what Q and Z mean: Q is 1 if we may be trying to dig the exit from the maze, and Z is 1 if we have already done this. This also means that Q is, strictly speaking, redundant. We can tell if we're trying to dig the exit just by looking at S and V! If we have selected the option to dig south, and we're on the bottom row, we are in fact digging the exit, thank you very much; we need no Q to keep track of this. More importantly, it means we can abstract away the entire dance involving Q and Z as being part of "is it OK to go south"—it's just that instead of doing a simple bounds-check and visitation-check like the other three directions, we have an extra rule that says that if we have not yet dug the exit, the bounds-check may be skipped. Now we can translate the whole 260-1000 block: pre:If it's OK to go west If it's OK to go north If it's OK to go east Dig in a random direction chosen between (North, East, West) otherwise If it's OK to go south Dig in a random direction chosen between (North, West, South) otherwise Dig in a random direction chosen between (North, West) otherwise If it's OK to go east If it's OK to go south Dig in a random direction chosen between (East, West, South) otherwise Dig in a random direction chosen between (East, West) otherwise If it's OK to go south Dig in a random direction chosen between (West, South) otherwise Dig west otherwise If it's OK to go north If it's OK to go east If it's OK to go south Dig in a random direction chosen between (North, East, South) otherwise Dig in a random direction chosen between (North, East) otherwise If it's OK to go south Dig in a random direction chosen between (North, South) otherwise Dig north otherwise If it's OK to go east If it's OK to go south Dig in a random direction chosen between (East, South) otherwise Dig east otherwise If it's OK to go south Dig south otherwise We're stuck; scan for the next occupied space, proceed to next iteration Now that we've laid out the entire set of logic, we can also wee what it does: it is selecting a random valid direction to dig, or, if it is completely boxed in, moving the cursor to the next cell that's still part of the maze. It's just that it's spending nearly seventy lines of code to do so, by exhaustively enumerating every possible case. (The case where all directions are valid is missing, but that is an impossible case; you start at an edge, and in every other case you had to arrive at the current cell from some direction that is now illegal.) This is... not the best way to perform this operation. Even in BASIC. Even without GOSUB. The alternative I devised was eight lines long, none of which were over 80 characters in length. Nevertheless, now we know what it's doing and how it does it. On the bright side, we can also now see that there's no problem with that GOTO 530 in the loopback code; that branch was only taken when digging east, and essentially just skipped the entire "if it is OK to go west" block. It had come from the west, so it was skipping a test that it knew was guaranteed to fail. We don't really need a DoFirstBit flag after all. "Needs a scan" also may be more sensibly rephrased as "we are stuck". The Big Picture We've been talking about moving cursors around and setting and checking various cells in a grid. That's not what we're here for. We're here to generate a maze. What's the top-level view of the program? pre:Get valid maze dimensions from user Select a random column (X) to start the maze Print northern edge of maze with entrance marked Mark (X,1) as visited Mark (X,1) as current location. Repeat until the maze and its exit have been fully dug out: Select a random valid direction if possible If it was not possible: Mark ourselves as stuck If it was possible, and we're not digging the exit: Dig in that direction, and set the current location to that new value If it was possible, and we *are* digging the exit: Open the exit If this cell has an exit to the east, mark ourselves as stuck. If this cell has no exit to the east: Change current location to (1,1) Mark ourselves as stuck if the (1,1) is still not dug out If we have marked ourselves as stuck: Scan through the grid left to right and top to bottom for the next visited cell Mark ourselves as not stuck Print resulting maze After sorting out how it worked, I created an equivalent but vastly shorter version of it, and then made some additional modest improvements. This post has gone on far too long as it is, though, so I will save that for another post. ManxomeBromide fucked around with this message at 01:36 on Jun 24, 2020 |
# ? Jun 23, 2020 22:12 |
|
EVEN MORE AMAZING Here is my improved version of Amazing, rewritten with sensible data structures and algorithms and noticeably improved graphics. It is compatible with QBASIC and GW-BASIC, and it is less than fifty lines of code, none of which are more than eighty characters long: code:
A Challenge There's still a bug lurking in this code somewhere, and it's visible in the screenshot. Can you find it? Commodore Conversion The Commodore 8-bits do not have the same graphics characters as the PC, but they do share some of them even when they are in different places. This program is pretty trivially portable to the 40-column Commodore computers (PET 4016, C64, C128, C16, C/+4) with only a few changes: code:
The VIC-20 has only 20-odd columns so you won't get a maze wider than 6 cells, but this code will work even on an unexpanded VIC-20 (3.5KB of RAM!)... as long as you shorten the prompt in line 40: code:
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.
|
# ? Jun 24, 2020 01:16 |
|
ManxomeBromide posted:There's still a bug lurking in this code somewhere, and it's visible in the screenshot. Can you find it? I see that there's a square that never gets entered, but I'm not sure what causes that. I'm also not too fussed about finding it; given how thoroughly you've dissected and reconstructed this program, it's no surprise that your watch is missing. Thank you very much! Carbon dioxide posted:This BASIC code is hard to read. I'll just wait for you folks to come up with the bit-by-bit explanation if you don't mind. I can't do anything about incomprehensibly, but I can improve the legibility, I think; I found a PDF scan of the book on Archive.org, and I'll pull pictures from it. Starting with this one! The actual listing is on the next page, but I'll save you the eyestrain and reproduce it in text. It's shorter and much more sensibly coded - except for one major sin. code:
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:
code:
code:
code:
code:
Manipulating strings code:
Finally, there's the <> operator. Comparing strings works by comparing the numbers used internally to represent the characters; the most typical set of these is ASCII, which defines printable characters in the 32-126 range and reserves 0-31 and 127 for other tasks. < means 'earlier in an alphabetical sort' while > means 'later in an alphabetical sort', but the definition of 'alphabetical' differs from your grade school library class; generally speaking, numbers come before letters and capital letters come before lower-case letters, but symbols are sprinkled throughout. "" < " " < "*" < "0" < "=" < "A" < "[" < "a" < "{"... in standard ASCII, at least. This is one of the places where BASIC varieties change the most. QuickBASIC and its descendants add the IBM's Code Page 437 characters, allowing you to use symbols like ▒ and Θ. The ZX Spectrum's character set includes symbols like Ł and ▚. And the Commodore 64 has two character sets, one with uppercase letters and many graphics symbols (for instance, ⬤ and ◤) and one with fewer graphics symbols but with lowercase letters instead - and to make matters more confusing, it's the graphic symbols in the first mode that become uppercase letters in the second, with the uppercase letters becoming lowercase. In what's called PETSCII, "a" < "A"! (Or, if you prefer, "A" < "♠".) Which caused no end of headaches trying to connect to non-Commodore computers, lemme tell ya. ...and speaking of headaches, why you replace that Unicode with an image, forum software? Anyway. Went on a bit of a tangent there. More string functions as they come up. In this specific case, since A$(K) is "\QDOES IT SWIM\Y2\N3\", the code starts at the third character, "D", and continues until - well, you'd think by looking at that code that it'd continue to the end, printing everything but the backslashes, right? Wrong. The NEXT command, being after the IF - THEN command, is only called if the IF part is true. The first time this code hits a backslash, it drops through to the next line - exiting the FOR - NEXT loop without finishing it. This is a VERY BAD THING. The computer keeps track of what loops it's got going in much the same way it keeps track of where it has to return when it's done with a subroutine. The memory it has to do so is limited. Eventually, it'll run out, and your program will crash with an OUT OF MEMORY error - most likely nowhere near the actual problem. And because memory usage can vary from run to run, you may not even be able to reliably reproduce the error! Fortunately, there's a fix for this. As I mentioned, variables have global scope. This includes loop variables. If we change line 415 to IF MID$(Q$,Z,1)="\" THEN Z=LEN(Q$)+1 and add a line, say, 417 that's PRINT MID$(Q$,X,1);:NEXT Z, then when Z reaches 15, the location of the backslash after SWIM, it'll be set to 22, the program will try to print the 22nd character of Q$ (but since there isn't one, won't print anything), and the loop will immediately end. Now where were we? code:
code:
(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:
code:
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:
code:
code:
code:
code:
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:
So. This game, unlike the previous program, is fairly neatly coded, aside from the aforementioned grave sin. There's a variable that's defined but never used, but we can get rid of that easily enough. (In case you're playing along on a Commodore 64 or something, typing a line number without anything after it will delete that line.) As it stands, though, it has two major limitations. For one thing, it has no way of saving the database from run to run; file I/O is so interpreter-dependent that I'm not going to even bother to suggest a fix. The other is that it only has room for 200 items (questions and answers) in its database, and when it runs out, it'll crash when it tries to store a string in A$(201) or above. (Can you see a good way to fix that?) Unfortunately, BASIC doesn't allow us to resize variable arrays once they're defined. (Technically, there is a way, but it involves erasing the values of all variables; the command, if you're curious, is CLR.) So there's only so smart this program can get in the short term, and it has no long-term memory. That said, you could take the output of a play session and put extra information into the DATA statements, because that's what you do with BASIC: rewrite other people's code, because the code is right there. So. As more than one of my college professors said, questions? Comments? Difficulties? For that matter, any BASIC anecdotes you want to share? FredMSloniker fucked around with this message at 05:08 on Jun 24, 2020 |
# ? Jun 24, 2020 05:03 |
|
So there's some Bad Stuff going on right now (in other news, welcome to 2020; you may have missed it), and the future of the forums is in question. I, for one, am in no hurry to hurl myself into the sea, but if the boat explodes or something, I've got my posts backed up, and I'll find a retro forum to resurrect the thread on. (If you have any suggestions in that regard, put them over here.) That said, anybody have anything they want to say or hear, or should I move on to the next program?
|
# ? Jun 24, 2020 21:07 |
|
The original Amazing looks very much like assembly. Either it was ported or the programmer just had that mental construct of it. If not, they must have been a big fan of flowcharts, which would probably be the only other way to arrive at that structure. It's good to see this sort of analysis of stuff I never really went back to. I hope that it can continue in some way or another.
|
# ? Jun 24, 2020 21:39 |
|
There's a strong connection between these old dialects of BASIC and 80s-era assembly language, to be honest. All variables are global because your chip isn't very good at stack variables, with GOTO and GOSUB serving almost exactly the same purpose as JUMP and CALL statements, and with every branch being some kind of IF ____ THEN GOTO statement. Even if it were designed for assembly language, generalizing "pick a random valid direction" would have been a vast improvement even there.
|
# ? Jun 25, 2020 01:38 |
|
I forgot to mention that I really like the way you implemented 'pick a random direction'. It's not dissimilar to how I would implement it in Lua (stuff valid elements into a sequence, then pick a random sequence member), and it also bears similarities to the deck-handling code I posted earlier. It's also given me an idea for improved deck-handling in general, which'll come in handy when I get to Blackjack. (Er, spoiler alert: there's a Blackjack program in this book.) For now, though, I'm going to cover the 'exercises' I mentioned regarding Animal. Here's a simple fix for the issue of jumping out of the X and Y loops: code:
(I may have said earlier, but back in my C64 days, I got used to using variables starting with Z as temporary values. ZZ, of course, was the most ephemeral of those, sometimes destined to be thrown away immediately in a timing loop.) Another way to get all but the first two characters of A$(K) (as seen in line 200) would be MID$(A$(K),3,LEN(A$(K))-2). However, since MID$() gracefully handles running out of characters, you could easily make that MID$(A$(K),3,LEN(A$(K)) - or, indeed, MID$(A$(K),3,999), presuming your version of BASIC can't have strings that long. This is why, incidentally, you would bother with using MID$() in this situation - so you don't have to also use LEN(). You could also amend my modified lines 460 and 490 with that in mind. On an unrelated note: it'd probably be a good idea to set the various variables used for input to "" before using INPUT. I've confirmed that, at least in Commodore BASIC V2, if you don't enter anything, it doesn't change the original value of the variable, and one carriage return too many could confuse the program. Also, there's no sanity check for animal names, so you could stick a backslash in the name or not type anything and really confuse matters. As for the assumptions made in lines 600 on, I said there were two. One is that the screen/paper is at least 60 columns wide. The other is that no animal will have a name longer than 11 characters. Both assumptions appear in line 624, which tries to format the animal names into pretty columns but only works if those constraints are met. Finally, to keep from overflowing the A$() array, I'd suggest checking the value of A$(0) before adding new information - ideally, after confirming that the player has a new animal in mind but before bothering to ask for information about it - and throwing some sort of complaint before returning to line 120. And if I were somehow in the Teletype age, I might consider adding a 'dump database' subroutine that would format and print BASIC DATA commands that I could then type back in to update the code and give it more brainpower! (If the teleprinter had a paper tape attachment, I could even write the lines to tape, then read them back in to make the changes automatically. That'd be one way to save and load data!) Anyway. I'll have more on the next program in the book tomorrow sometime; right now I'm nursing a broken foot. (It's been broken for a couple months now, and I'm back in regular shoes, but I'm still being careful about long periods of verticality, and that can include sitting.) Next time, though, we see a game that's actually halfway worth playing! FredMSloniker fucked around with this message at 21:34 on Jun 25, 2020 |
# ? Jun 25, 2020 02:24 |
|
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:
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:
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:
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:
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:
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:
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:
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:
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:
Let's take a look at the rest of the J loop again. code:
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:
code:
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:
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:
The code, unfortunately, makes a misstep here, though not a lethal one. Unlike some languages, in BASIC a FOR - NEXT loop is guaranteed to run at least once. The intent of the code in line 860 is to compare the current game to previous games, but if we haven't had any previous games, we wind up comparing it to itself. I'd change line 850 to end IF C>8 OR N=0 THEN 875. If the comparison in line 860 is true, then we subtract 2 from Q. Since Q is the score the computer expects to have after making the move it's considering, this says 'if we've been in this situation before, and didn't win, this position isn't as good as we think it is'. So why isn't the comparison just IF F(N)=F(I)? That is the final mystery. Let's suppose that the first move the player makes is from their fourth pit (internally, pit three), which puts a seed in their home pit, giving them a second move. They then move from their first pit (internally, pit zero), putting their last seed in the now-empty pit and making a capture. Let us suppose further that the player has already won or drew a game. It is now the computer's move. N=1; F(N)=6*3+0=18; C=2; and K is the move the computer is currently contemplating, from 0 to 5. F(N)*6+K is therefore what will be stored in F(N) if the computer actually makes this move. F(0) is the first eight moves of the first game the player won or drew. ^ is BASIC's exponentiation operator, so 6^(7-C) is 65. Since dividing by six once removes the information from the previous move, dividing by six five times in a row removes the information from the five previous moves - and eight minus five is three. So now we have the first three moves of that previous game... and a ratty bit of fraction, since F() is a floating-point array. (This variety of BASIC doesn't have integer variables.) We get rid of them with the INT() function - but we add .1 first. Why? Well, .1 is less than 1/6, so this won't ever push the fractional bit over 1, but it will handle any fractional weirdness where dividing 6 by 6 somehow produces 0.9999. (It has to do with how floating-point numbers in general, not just in BASIC, are handled. BASIC just handles it more poorly than other languages.) In other words, if this third move would replicate the first three moves of a previous game - and if the player used this strategy in that game, at least one of the moves the computer's considering will - we mark that move as not being as good as we think. This is likely to make the computer choose a different move, which will hopefully increase its chances of winning, and if it doesn't, well, that's more data for next time. And if the player keeps winning by using those first two moves, the computer is going to be more and more unlikely to choose any option that led to a loss, since it can subtract two more than once. Once we've made eight moves, we're in unknown territory, since that's as far as the computer can keep track of. At that point, it stops calling the 855-870 code and does the best it can. There's a bug in this code, incidentally. If C=8, C^(7-C)=1/6, so we wind up multiplying F(I) by 6. C is updated after this part of the code, so C=8 here means the computer's making its ninth move. Line 850 should end IF C>7 OR N=0 THEN 875. That Honking Big Asterisk So that thing I said about F() storing the first eight moves of a given game? Weeeell... that depends on F() being capable of holding a number as large as 68-1, or 1679615, without issue. Fortunately, most varieties of BASIC can handle that. Unfortunately, they may not be able to handle it perfectly. I mentioned floating point weirdness; because of the way these numbers are stored internally, a number that displays as 6 may actually be something like 5.9999997 or something, which is why we added .1 in line 860. So, you might say, your variety of BASIC supports integer arrays. Why not just use those? Well, you can, but there are a few things to be careful of. One is that the behavior you get when you divide an integer variable by something else also depends on your variety of BASIC. 11/6 may be 1.8333... and get rounded down by INT() to 1, but F%(N)/6, with F%(N) containing the integer 11, may give 1.8333... or it may give 1. Or 2, the division rounding to the nearest integer instead of rounding down. The other thing you need to be aware of is that integers have limits, just like floating point numbers do. With floating point numbers, it's generally significant digits (i.e, you might have trouble storing exactly 1.795668586868469) but with integers, it's a hard limit on the minimum and maximum values - and in, say, QuickBASIC, that maximum limit is 32767! Fortunately QuickBASIC also provides the long integer type (the & suffix), which goes up to around two billion (precisely, 2147483647), but, say, Commodore BASIC V2 does not. If your flavor of BASIC provides neither sufficiently accurate floating point numbers nor sufficiently large integers, you could change F() to have two dimensions, so F(N, C) would hold the Cth move of game N. You'd replace line 860 with a loop through the second dimension's elements, looking for all of them to match your current game's up until the point you're at. Still. This program, once you deal with the major sins of 'jumping out of loops' and 'assuming a certain precision from variables', is a compact, well-organized program that plays an acceptable game of 'Awari' and learns from its mistakes. It'd be relatively easy to change it to follow the actual rules of Kalah, and the AI could be adapted to any reasonably simple game. I didn't expect much from this program going in, and I'm pleased by just how wrong I was! In case you want to try it for yourself, here's the full, bug-patched listing. (It also fixes a bug I didn't mention: if N>50, F(N) is invalid. The fixed program will just stop learning at that point. I also added something to keep pressing enter at the input prompt from doing something unexpected.) It'll work on even a 40-column monitor, provided you adjust lines 5-7. Omit the comment on line 520, or else the line will be too long for (at least) Commodore BASIC V2. code:
FredMSloniker fucked around with this message at 17:07 on Jun 26, 2020 |
# ? Jun 25, 2020 21:24 |
|
I was going to ask "What if the computer's move ends the game? Its AI doesn't have a check for that." Apparently, you also spotted that. EDIT: Actually, it looks like it also doesn't consider its own second move when evaluating the maximin. It just makes one move for itself and one for the player, although having made its first move, it will reconsider all of that for the second move. Nidoking fucked around with this message at 00:09 on Jun 26, 2020 |
# ? Jun 26, 2020 00:04 |
|
Nidoking posted:I was going to ask "What if the computer's move ends the game? Its AI doesn't have a check for that." Apparently, you also spotted that. You're correct on both counts. The AI assumes that, after the computer's move, the player will be the next one to make a move. This could be untrue because the move it's considering ends the game or because the move it's considering gives it a second move. Fortunately, this never causes the game to outright crash, but it does cause it to mis-weigh its options. Fixing this is possible, but it isn't trivial. The subroutine at 800 needs to know whether it's the computer's first move or its second to know if its move could lead to a second move; we can provide that by changing lines 60 and 70 to set some variable. We also have to remove line 240 from its current position and put it somewhere else so that we can check if a certain move would end the game without falsely updating the AI's memory. Last but not least, the subroutine at 800 has to be changed to actually account for those possibilities. If anyone wants to come up with a patch, please do! As for me, I'll be preparing for my coverage of the next program in the book. The preparations will involve a lot of grunting. I'll be quite out of grunts when I'm done. FredMSloniker fucked around with this message at 05:40 on Jun 26, 2020 |
# ? Jun 26, 2020 01:43 |
|
When I was little I read those books so much the bindings fell apart. Sometimes the typos and the syntax differences from GWBASIC really messed with me after spending a whole afternoon keying in a program. My first real programming job involved a lot of Hewlett-Packard BASIC, maybe Dijkstra was right.
|
# ? Jun 26, 2020 01:54 |
|
loaf posted:My first real programming job involved a lot of Hewlett-Packard BASIC, maybe Dijkstra was right. He wasn't. (The quote in question, for those unfamiliar with it, is "It is practically impossible to teach good programming to students that have had a prior exposure to BASIC: as potential programmers they are mentally mutilated beyond hope of regeneration.")
1980s BASIC is never the right approach, these days. I'll give Dijkstra that. But if you can't write something, even in 1980s BASIC, that is recognizably a map-reduce operation, you don't really fully grasp map-reduce. At best you can command it.
|
# ? Jun 26, 2020 02:42 |
|
FredMSloniker posted:Okay. Remember how I said BASIC has subroutines? I was a filthy liar. The only difference between GOTO and GOSUB is that GOSUB leaves a note on the top of what's called a stack saying where to go the next time the program RETURNs. This is very fun, and the optimization that happens here can be both (1) made even more awful and (2) done mechanically. The more awful bit: if that GOSUB were to a routine that started right after this one, you would not even need the GOTO. You could simply let control fall into the second function directly. As for doing it mechanically... there are two transformations you can do to make this happen "automatically:"
FredMSloniker posted:The other thing you need to be aware of is that integers have limits, just like floating point numbers do. With floating point numbers, it's generally significant digits (i.e, you might have trouble storing exactly 1.795668586868469) but with integers, it's a hard limit on the minimum and maximum values - and in, say, QuickBASIC, that maximum limit is 32767! Fortunately QuickBASIC also provides the long integer type (the & suffix), which goes up to around two trillion (precisely, 2147483647), but, say, Commodore BASIC V2 does not. Nitpick: you're off by a factor of a thousand. That is two billion. That is also the range of a 32-bit signed integer, still a very common data range in modern programs!
|
# ? Jun 26, 2020 03:14 |
|
ManxomeBromide posted:Nitpick: you're off by a factor of a thousand. That is two billion. That is also the range of a 32-bit signed integer, still a very common data range in modern programs! To quote myself, FredMSloniker posted:You're correct on both counts. I've amended the post accordingly. Interestingly, Commodore BASIC uses 16-bit signed integers, but... 40-ish bit floating point numbers? I don't entirely understand the specification.
|
# ? Jun 26, 2020 03:32 |
|
Is it legal to do a complete for loop in a single line? so FOR blabla : some operation : NEXT
|
# ? Jun 26, 2020 06:55 |
|
Holy crap I had this book as a kid and spent hours with it on my Dad's IBM PC.
|
# ? Jun 26, 2020 07:01 |
|
Carbon dioxide posted:Is it legal to do a complete for loop in a single line? so FOR blabla : some operation : NEXT Yes. Just, as noted in the "Animals" commentary, make sure that the NEXT always executes, and you really should make sure that there is exactly one NEXT for every for. It is theoretically possible to write code that handles this correctly, but seriously, just don't. Put the NEXT at the very end and conditionally jump to it as needed. NEXT is not C's "continue". Some operations (IF, for instance) track by line number, where failing a test skips the whole rest of the line. They're the unusual cases. Normally, things like GOSUB or NEXT or FOR or whatever track at the individual statement level, not the line level.
|
# ? Jun 26, 2020 07:46 |
|
Our BASIC codebase was pretty clean and organized, but this was a common pattern and source of weird bugscode:
|
# ? Jun 26, 2020 19:14 |
|
loaf posted:Our BASIC codebase was pretty clean and organized, but this was a common pattern and source of weird bugs Eugh. I'd set Loopify to whatever number of loops you wanted and have I go from 1 to that, get rid of the IF - THENs altogether. And speaking of 'eugh'... Nnnnnnnnnnnngh. NNNNNNNNNNNNNNNNNNGH. Okay, I get it. Guess-the-number games have probably been around since there were numbers. For computers especially, they're super easy to code, and they don't require any particular genius to learn to play. But they're so boring, and there are so many of them. In this book alone, there are eight games that involve you trying to figure out a random number or other pattern - and that's not counting things like 'guess the correct set of coordinates' or 'guess the word'. Still. I choose to look at the bright side. And the bright side, in this case, is that this program does what most of the programs in this collection don't: bulletproof, to an almost excessive degree, its input. So I'm going to focus on that part of the program. code:
So the player has typed in something, and we've stored it in A$. Now what? code:
code:
code:
code:
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:
code:
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:
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:
code:
code:
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:
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:
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:
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:
code:
code:
So! What're the major problems with this program? Well, there are jumping out of FOR - NEXT loops and a lack of input checking, which are nothing new - though really, it should at least check to see if it's going to overflow a single line before the user goes through a box of paper printing "HAPPY BIRTHDAY PRINCESS CELEST" or something. I'm also obscurely disappointed that you can't use "ALL" to print pixels when you could use "LOVE" or something, but hey. There's also no error handling for typing a symbol that isn't in the program's set of known symbols. Incidentally, I'd read all of that data into a large array or two instead of having to jump back and forth through it, but if memory's a concern, this approach works. (I note that you could order the DATA lines in descending frequency of symbol use, which would at least speed up the 'start from the beginning and go until you find the symbol you want' approach. It looks like the author has made a vague effort in that direction.) Other than that, though, it does what it says on the tin. It just isn't worth using for anything if you're not on a teleprinter. There are varieties of BASIC that let you send things to the printer, but they are (naturally) interpreter-dependent, and also hardware-dependent - early varieties of BASIC make assumptions about what a printer will do that may not be supported by modern printers, or by modern operating systems. For instance, you can print text from within the VICE emulator - but it gets stored in a text file, and you have to choose between 'contains text, but doesn't support the Commodore's special characters' or 'contains a pixel-by-pixel representation of the output with plus symbols for black pixels'. And even if you could run QBASIC under Windows 10, I dunno what'd happen if you tried to LPRINT. So. One program that isn't useful anymore and one that wasn't really useful then. I'm looking forward to the next program, which is actually kind of interesting-looking. FredMSloniker fucked around with this message at 03:07 on Jun 27, 2020 |
# ? Jun 27, 2020 00:38 |
|
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.)
|
# ? Jun 27, 2020 00:40 |
|
You know, I was thinking and the best BASIC games I've ever seen are probably GORILLA.BAS and NIBBLES.BAS that came with MS-DOS QBasic. Those are way more complex than anything in this book and you'd probably wouldn't want to copy them from paper. Anyway, I guess the stuff they do where they have a sort of full screen graphical game where the whole screen updates instead of the terminal just scrolling as it prints things is something for which you need a much more modern computer than anything this book was aiming for.
|
# ? Jun 27, 2020 07:53 |
|
|
# ? May 3, 2024 05:17 |
It's more that doing full screen graphics or pseudo-graphics typically needs some kind of hardware specific knowledge. Every system tends to have its own way of setting graphics modes, colors, or moving the text cursor around the screen. It wouldn't be easy to make those portable between different systems. One way might be to leave gaps in the program for the system specific things, and then have a separate section in the text for "on system A enter these additional lines, on system B enter these instead, on other systems you're on your own".
|
|
# ? Jun 27, 2020 10:58 |