Category Archives: BASIC

An Interactive BASIC with a Punch-Card Mentality

What was included and what wasn’t in the very first version of BASIC might surprise you. First of all, Dartmouth was waiting for the time-sharing system equipment from GE to arrive, but in the meantime Kemeny and Kurtz decided to write a compiler for BASIC, to have it ready once when implementation of the time-sharing system began.

While the two had designed BASIC to be an interactive language, with the goal of removing students from the tyranny of interacting with computers through batch processing of punch cards, the two still envisioned computing through a punch-card paradigm. And, in fact, that very first version of the BASIC compiler required punched cards.

Before the time-sharing environment was ready, the card-operated version of the BASIC language went live, CARDBASIC, permitting one line per card. Because of differences between the teletype and the keypunch, PRINT used a single quote ‘ instead of a double quote “, and the relational operators were EQU, LSS, GRT, LQU, GQU, and NQU instead of =, <, >, <=, >=, and <> respectively. The soon-to-be-removed PAGE instruction started a new printed page. And the famous MAT instructions offered students using the language access to matrix math: MAT READ A(M,N) instead of a FOR-loop, MAT PRINT A, MAT C=A+B and so on, including matrix functions CON, IDN, INV, TRN, and ZER.

While later iterations of BASIC would include file input and output, the first Dartmouth BASIC instead had READ and DATA commands, even with the interactive version. Like a program typed on punch cards, the data would be included with the program stored in the time-sharing system. Here’s the very first example program from the first BASIC manual (October, 1964; page 3):

10 READ A1, A2, A3, A$
15 LET D = A1 * A4 – A3 * A2
20 IF D = 0 THEN 65
30 READ B1, B2
37 LET X1 = (B1*A4 – B2*A2) / D
42 LET X2 = (A1*B2 – A3*B1) / D
55 PRINT X1, X2
60 GO TO 30
65 PRINT “NO UNIQUE SOLUTION”
70 DATA 1, 2, 4
80 DATA 2, -7, 5
85 DATA 1, 3, 4, -7
90 END

In fact, as its card-based pilot implementation did, the first interactive version of BASIC lacked an INPUT command. The only way to tailor data was either to hardcode it in variable LET assignments or to use the READ/DATA capability. While BASIC was meant to let you run commands and see the results immediately, there was no way to write a program that would accept input from the user. (There was no INKEY$ command either, as there were no string variables.) Kemeny and Kurtz thought it was too difficult to implement an input statement in a time-sharing system and instead wanted to provide quick execution of small programs. (Within two years, BASIC would have an INPUT command.) [See Back to BASIC, page 25.]

Here’s the first version of the language:
DIM (), …, (, )
LET =
MAT =
(MAT) READ , , …,
DATA , , …,
PRINT

Note that only a line number could follow a THEN in an IF statement (not other statements, like later IF … THEN PRINT X: GOTO 10). Also, only IF statements could have expressions with relational operators: LET F = A<10 wasn’t allowed. Variable names could be a letter or a letter followed by a digit, mainly to make it simpler to develop the compiler. (Array names, for the same reason, were a single letter.) Note that PRINT didn’t require a delimiter between a literal string (called a “label”) and a following expression: e.g. PRINT “SINE ” SIN(X). PRINT did allow the suppression of the carriage return by using a trailing comma (PRINT I,) though it instead advanced the cursor to the next print zone.

Arithmetic used nine digits of precision for integers and supported floating-point operations: the operators were + – * / ^. Moreover, the first BASIC had a rich library of functions: ABS, ATN, COS, EXP, INT, LOG, RND, SIN, SQR, and TAN. RND ignored its argument and generated a reproducible sequence of random numbers one at a time from 0 to 1 (the same numbers would occur in every program run). Programmers could even define their own single-line functions, FNA through FNZ, but the parameter passed in overrode it’s corresponding variable; e.g., DEF FNL(X) = LOG(X)/LOG(10) would assign the passed-in parameter to X! In other words, all variables were global, and functions had the side effect of changing the value of a global variable.

Once the time-sharing system went live, command-line instructions were added to the language, and included HELLO (to start the user-login procedure), NEW (to create a new program and name it), OLD (to load an old program), SCRATCH (to erase the program but keep the name), RENAME, CATALOG, SAVE, UNSAVE, and LIST (including LIST–70 to list from line 70 on, for example).

The implementation also had two pages of verbose (by later microcomputer standards) error messages, from “ILLEGAL FORMULA” to “END IS NOT LAST”, but strangely no divide-by-zero message. (Microcomputer BASICs would have two-letter error codes or error numbers or, in the case of Palo Alto Tiny BASIC, just three messages: “How?”, “What?”, and “Sorry.”)

The first version of BASIC was intended as an alternative to FORTAN and ALGOL for students, and it was larger and more powerful than the first generation of Tiny BASICs for microcomputers, which would follow 12 years later.

The Tiny BASIC Interpretive Language IL—and Onions

Tiny BASIC implementation onion diagram

In the fall of 1975, the People’s Computer Company (PCC) poured more energy into their “Build Your Own BASIC” project, fired up by the announcement of Altair 4K BASIC by MicroSoft [as it was then] for $150. They wanted to create an implementation that was free for anyone to use. With microcomputers and microprocessors proliferating, they envisioned a virtual machine that could be ported from platform to platform:

When you write a program in TINY BASIC there is an abstract machine which is necessary to execute it. If you had a compiler it would make in the machine language of your computer a program which emulates that abstract machine for your program. An interpreter implements the abstract machine for the entire language and rather than translating the program once to machine code it translates it dynamically as needed. Interpreters are programs and as such have theirs as abstract machines. One can find a better instruction set than that of any general-purpose computer for writing a particular interpreter. Then one can write an interpreter to interpret the instructions of the interpreter which is interpreting the TINY BASIC program. And if your machine is micro-programmed (like PACE), the machine which is interpreting the interpreter interpreting the interpreter interpreting BASIC is in fact interpreted.

This multilayered, onion-like approach gains two things: the interpreter for the interpreter is smaller and simpler to write then an interpreter for all of TINY BASIC, so the resultant system is fairly portable. Secondly, since the main part of the TINY BASIC is programmed in a highly memory efficient, tailored instruction set, the interpreted TINY BASIC will be smaller than direct coding would allow. The cost is in execution speed, but there is not such a thing as a free lunch.

“The machine … interpreting the interpreter interpreting the interpreter interpreting BASIC is in fact interpreted.” These guys were having fun! Geeky fun, but fun.

While writing compilers was just being generalized (lex and yacc were published circa 1975), PCC recognized that hobbyists wouldn’t have access to these new tools. Writing a parser is a very specific domain. Accordingly, they created a simple language, IL (Interpretive Language), to write their Tiny BASIC in and encode the parsing in.

Let’s look at a representative example:

 S1: TST     S3,'GO'       ;GOTO OR GOSUB?
     TST     S2,'TO'       ;YES...TO, OR...SUB
     CALL    EXPR          ;GET LABEL
     DONE                  ;ERROR IF CR NOT NEXT
     XFER                  ;SET UP AND JUMP

A common pattern in IL is to test for a keyword or token, then act on that information. Each test is an assertion as to what is next in the line buffer. If the assertion fails, control jumps to a subsequent label (usually looking for a new keyword or token). Here the system advances its buffer cursor over any spaces and tests for “GO” and if it fails to find it then jumps to line “S3”. If it finds it, execution continues with the next IL command. In this case, the system next tests for “TO”, skipping to line “S2” if it fails (a test for “SUB”, to see if this is instead a GOSUB command). If it passes, control continues; in this case, calling an IL subroutine that starts at label “EXPR”, which parses an expression. In Tiny BASIC, “GOTO X*10+100” (a computed GO TO) is as legal as “GOTO 100” and is the alternative to the ON-GOTO of larger BASIC implementations. The subroutine “EXPR” pushes the result of the expression onto the arithmetic stack (in this case, the line number). “DONE” verifies no other text follows the expression and gives an error if it does. “XFER” pops the number of the stack and transfers (GOTOs) the corresponding line number, if it exists.

The IL listing was concise: just 124 lines long! Just 124 lines to implement a workable BASIC that you can write programs and even games in. It’s very elegant.

IL is simple enough that I thought I could program it in a high-level language in an evening. (It took two, plus another to debug.) I wrote a two-pass assembler from the specs, implementing each command. Then I assembled the program, using the listing that Tom Pittman had shared and corrected a few errors in. As far I can tell, I’m the first person to actually write an IL assembler in the 42 years the language has been around. (Tom had his own version of the IL: he changed the mnemonics to two characters each, to be easier to parse on a microcomputer, and modified some commands, and added others.)

I encountered three different types of problems.

The first class of problems were mental mistakes in the listing and the spec:

  • “CALL VAR” needed to be “TSTV S17”, as the listing has no VAR routine but does have a built-in command to test a variable.
  • The use of “MPY” in the listing for the “MUL” command (all five math functions in IL are the first three letters of their corresponding word: ADDition, SUBtraction, MULtiplication, DIVision, NEGation).
  • The language specification neglected to define “LIT”, which pushes the following byte onto the arithmetic stack.

The second problem was that the language hardcodes jumps: for instance, “DONE” jumps to the third instruction (label “CO”). For what is essentially a finite state machine, I found this to be pretty insufferable. In my implementation, I changed commands to take their destination label. (In defense of PCC, they had to worry about carefully managing 4KB of RAM. I have 16GB: 4 million times more.) I later discovered that Tom Pittman changed “DONE” to “BE” (“Branch if Not Endline”) with a label as his solution to this issue.

The third problem with the program was subtler, and escaped my notice at first. Typically, “RUN” resets all variables (in this case, the 26 variables A to Z) to zero. There’s nowhere for that to happen in the listing. You could do this programmatically in IL:

1
0
STORE
2
0
STORE
…
26
0
STORE

However, 78 added instructions to a listing of 124 instructions (not counting comments) seemed excessive.

Sadly, while IL uses a stack machine, it doesn’t provide any way to actually manipulate the stack: no DUP, no SWAP, no conditional jump based on the top of the stack. If it had, the following would have been possible:

LIT 26    ; START AT THE 26TH VARIABLE
VINIT: DUP      ; DUPLICATE THE TOP OF THE STACK
LIT 0      ; PUSH 0
STORE   ; STORE 0 INTO THE VARIABLE
LIT 1      ; PUSH 1
SUB       ; SUBTRACT 1 FROM THE VARIABLE INDEX
DUP       ; DUPLICATE
JMPC VINIT  ; JUMP IF NOT 0

So I implemented a VINIT keyword to do this instead.

Tom Pittman notes:

The TINY BASIC interpreter was designed by Dennis Allison as a Recursive Descent parser. Some of the elegant simplicity of this design was lost in the addition of syntactical sugar to the language but the basic form remains. The IL is especially suited to Recursive Descent parsing of TINY BASIC because of the general recursive nature of its procedures and the simplicity of the TINY BASIC tokens. The IL language is effectively optimized for the interpretation of TINY. Experience has shown that the difficulty of adding new features to the language is all out of proportion with the nature of the features. Usually it is necessary to add additional machine language subroutines to support the new features. Often the difficulty outweighs the advantages.

This is from The Tiny BASIC Experimenter’s Kit that he wrote 40 years ago. It is well worth reading in detail, as his is the only Tiny BASIC implementation to use an IL. (The first Tiny BASIC, TBX, used the IL as pseudocode for the outline of the machine language program.)

Here are additions I made to the IL to extend my own Tiny BASIC:

  1. I added a single array A(), a la Palo Alto Tiny BASIC’s @(), which can take a subscript from 0 to 255. (But note that subscripts of 230 to 255 map to the values of variables A to Z, since I use the “IND” operator to reference variables.)
  2. I added RND(X), which returns a pseudorandom number from 1 to X.
  3. I added ASC(“A”), which can take as an argument “A” to “Z” and returns the ASCII value from 65 to 90. (This limitation is because it uses the TSTV function as a test for a letter of the alphabet.) I added this because I extended INPUT A to return the ASCII value of the first character if a number is not entered. This is so that rather than “1 FOR YES OR 2 FOR NO” you can write “(Y)ES OR (N)O” and IF A=ASC(“Y”) THEN.

If you have an iPhone, you can download the free version of LowRes Coder and use my Tiny BASIC on your phone.

Whatever its merits as a language, IL helped inspire a proliferation of BASICs to compete with Microsoft. There was Tiny BASIC Extended       , Denver Tiny BASIC, 6800 Tiny BASIC, MINOL, Palo Alto Tiny BASIC, Integer BASIC, and Micro Basic. Palo Alto Tiny BASIC became the basis of TRS-80 Level I BASIC, which launched the TRS-80. Of course, Microsoft BASIC ultimately triumphed, replacing Integer BASIC on the Apple, Level I BASIC on the TRS-80, and being the only BASIC for the Commodore PET. And even today you can buy a Microsoft BASIC for your own use, as part of Visual Studio. Yet IL remains an important landmark in the history of personal computing.

Modified Tiny BASIC IL Listing

;THE IL CONTROL SECTION
START: INIT                  ;INITIALIZE
NLINE                 ;WRITE CRLF
CO:    GETLINE               ;WRITE PROMPT AND GET LINE
TSTL    XEC      ;TEST FOR LINE NUMBER
INSERT                ;INSERT IT (MAY BE DELETE)
JMP     CO
;
;STATEMENT EXECUTOR
XEC:   XINIT                 ;INITIALIZE
STMT:  TST     S1,'LET'      ;IS STATEMENT A LET
TST     S1A,'A('
CALL    EXPR
TST     SYN,')'
JMP     S1B
S1A:   TSTV    SYN           ;YES, PLACE VAR ADDRESS ON AESTK
S1B:   TST     SYN,'='       ;(THIS LINE ORIGINALLY OMITTED)
CALL    EXPR          ;PLACE EXPR VALUE ON AESTK
DONE    SYN           ;REPORT ERROR IF NOT NEXT
STORE                 ;STORE RESULT
NXT     STMT          ;AND SEQUENCE TO NEXT
S1:    TST     S3,'GO'       ;GOTO OR GOSUB?
TST     S2,'TO'       ;YES...TO, OR...SUB
CALL    EXPR          ;GET LABEL
DONE    SYN           ;ERROR IF CR NOT NEXT
XFER    STMT          ;SET UP AND JUMP
S2:    TST     SYN,'SUB'     ;ERROR IF NO MATCH
CALL    EXPR          ;GET DESTINATION
DONE    SYN           ;ERROR IF CR NOT NEXT
SAV                   ;SAVE RETURN LINE
XFER    STMT          ;AND JUMP
S3:    TST     S8,'PRINT'    ;PRINT
S4:    TST     S7,'"'        ;TEST FOR QUOTE
PRS                   ;PRINT STRING
S5:    TST     S6,','        ;IS THERE MORE?
SPC                   ;SPACE TO NEXT ZONE
JMP     S4            ;YES JUMP BACK
S6:    DONE    SYN           ;ERROR IF CR NOT NEXT
NLINE
NXT     STMT
S7:    CALL    EXPR
PRN                   ;PRINT IT
JMP     S5            ;IS THERE MORE?
S8:    TST     S9,'IF'       ;IF STATEMENT
CALL    EXPR          ;GET EXPRESSION
CALL    RELOP         ;DETERMINE OPR AND PUT ON STK
CALL    EXPR          ;GET EXPRESSION
TST     SYN,'THEN'    ;TEST FOR THEN
CMPR    STMT          ;COMPARE -- PERFORMS NXT IF FALSE
JMP     STMT
S9:    TST     S12,'INPUT'   ;INPUT STATEMENT
S10:   TSTV    SYN           ;YES, PLACE VAR ADDRESS ON AESTK
INNUM                 ;MOVE NUMBER FROM TTY TO AESTK
STORE                 ;STORE IT
TST     S11,','       ;IS THERE MORE?
JMP     S10           ;YES
S11:   DONE    SYN           ;MUST BE CR
NXT     STMT          ;SEQUENCE TO NEXT
S12:   TST     S13,'RETURN'  ;RETURN STATEMENT
DONE    SYN           ;MUST BE CR
RSTR                  ;RESTORE LINE NUMBER OF CALL
NXT     STMT      ;SEQUENCE TO NEXT STATEMENT
S13:   TST     S14,'END'
FIN     CO
S14:   TST     S14A,'LIST'   ;LIST COMMAND
DONE    SYN
LST
NXT     STMT
S14A:   TST    S15,'LLIST'
DONE    SYN
LLST
NXT     STMT
S15:   TST     S16,'RUN'     ;RUN COMMAND
DONE    SYN
VINIT                 ;(NEW COMMAND TO CLEAR VARIABLES)
LIT     1             ;(NEED TO TRANSFER TO FIRST LINE OF PROGRAM)
XFER    STMT          ;(XFER MODIFIED TO FIND NEXT HIGHER LINE # RATHER THAN PRODUCE AN ERROR)
S16:   TST     SYN,'CLEAR'   ;CLEAR COMMAND
DONE    SYN
JMP     START
SYN:   ERR     CO            ;SYNTAX ERROR ***
EXPR:  TST     E0,'-'
CALL    TERM          ;TEST FOR UNARY -.
NEG                   ;GET VALUE
JMP     E1            ;NEGATE IT
E0:    TST     E1A,'+'       ;LOOK FOR MORE
E1A:   CALL    TERM          ;TEST FOR UNARY +
E1:    TST     E2,'+'        ;LEADING TERM
CALL    TERM
ADD
JMP     E1
E2:    TST     E3,'-'        ;ANY MORE?
CALL    TERM          ;DIFFERENCE TERM
SUB
JMP     E1
E3:    RTN                   ;ANY MORE?
TERM:  CALL    FACT
T0:    TST     T1,'*'
CALL    FACT          ;PRODUCT FACTOR.
MUL
JMP     T0
T1:    TST     E3,'/'
CALL    FACT          ;QUOTIENT FACTOR.
DIV
JMP     T0
FACT:  TST     F01,'RND('    ;(ADDED RND(X) FUNCTION WHERE X>1)
CALL    EXPR          ;***
TST     SYN,')'       ;***
RND                   ;(ADDED RND COMMAND TO PUSH # ON AESTK)
RTN                   ;***
F01:   TST     F0A,'ASC(''   ;(ADDED ASC("A") FUNCTION)
TSTV    SYN           ;***
TST     SYN,'')'      ;***
LIT     166           ;(A = 231 IN MEMORY SCHEME)
SUB                   ;(REDUCE BACK TO 65 TO 90)
RTN                   ;***
F0A:   TST     F0B,'A('      ;(ADDED 1 BUILT-IN ARRAY)
CALL    EXPR          ;***
TST     SYN,')'       ;***
IND                   ;(CHANGED IND TO SUPPORT 0 TO 255)
RTN                   ;***
F0B:   TSTV    F0            ;***
IND                   ;YES, GET THE VALUE.
RTN
F0:    TSTN    F1            ;NUMBER, GET ITS VALUE.
RTN
F1:    TST     F2,'('        ;PARENTHESIZED EXPR.
CALL    EXPR
TST     F2,')'
RTN
F2:    ERR     CO            ;ERROR.
RELOP: TST     R0,'='        ; 0, =; 1, <; 2, <=; 3, <>; 4, >; 5, >=
LIT     0             ;=
RTN
R0:    TST     R4,'<'
TST     R1,'='
LIT     2             ;<=
RTN
R1:    TST     R3,'>'
LIT     3             ;<>
RTN
R3:    LIT     1             ;<
RTN
R4:    TST     SYN,'>'
TST     R5,'='
LIT     5             ;>=
RTN
R5:    TST     R6,'<'
LIT     3             ; ><
RTN                   ;(THIS LINE ORIGINALLY OMITTED)
R6:    LIT     4             ; >
RTN
EOF:                         ;(ENDS INPUT)

BASIC Microgames in 24 Lines

Growing up, I remember going to friends’ houses and typing in BASIC programs. We’d take turns entering the listing and double checking it, then play whatever game it was. Back then, everyone had a different microcomputer. I had a TRS-80 Model I, the library had an Apple II (too expensive for most of us back then), Ron had a VIC-20, Mark had a TI-99/4A, Mike had a Commodore 64, Chris an Amiga, and so on. Even the Bally Astrocade had a BASIC cartridge.

The iOS app LowRes Coder returns us to those glory days of BASIC programs, though without line numbers and with some structured programming: WHILE loops, REPEAT-UNTIL loops, DO loops, and multiline IF / ELSE IF / ELSE / END IF structures (heck, TRS-80 Level I BASIC didn’t even have an ELSE statement, let alone anything but a FOR loop!). No argument passing into subroutines though: still GOSUB and global variables.

The free version of the LowRes Coder app only lets people write 24-line programs. An arbitrary limit, yet a limit that inspired one user, Was8bit, to post this challenge:

I’ve posted several microgames … I challenge others to post their own microgames; it’s a fun challenge. Must follow these guidelines:

1) 24 lines of code, no more than…

2) Highlight one or a few programming skills or techniques

3) Should help give a new programmer a place to start learning by tinkering with your code, and/or have a concept that inspires similar games….

I’ve burned myself out with it, so I pass the torch to others… would like to see you try at least one microgame to help contribute 😀

The nice thing about a 24-line program is that it can be written and debugged in an evening, so it has a realizable end state. And it actually has some value as a prototyping tool (more on that in a bit). Or as the working version for a planned, fuller game, to be developed agilely.

My initial thinking was that only the classic Teletype interface programs where you enter numbers would be suitable for microgames: Guess, Lemonade Stand, Hamurabi, Lunar Lander, etc. To that end, I wrote Lemonade Stand 24.

But once that was done I decided to take another stab at a Connect Four clone. I had written it on the Apple II at our public library, inspired by my dad’s Tic-Tac-Toe program for our TRS-80. (Dad had built a Tic-Tac-Toe machine on a GENIAC as a kid.) I definitely had to compromise to get something to work in 24 lines: you can’t win by playing diagonally and the AI is pretty poor. (Check out The Complete Book of CONNECT 4: History, Strategy, Puzzles.) Clearly this is a programming exercise that would be mainly about the AI rather than the UI. Here it is: 4 In A Row.

I’ve been wanting to replay the Trek 80 program from my TRS-80 – it was my favorite game back then, but I haven’t been able to track it down. Turns out there were a lot of different Trek implementations. This video gives you a taste of the experience in the 1970s, though my version didn’t have those snazzy graphics: my version had =-0 for the Enterprise and >-0 for the Klingons. It did show a photon torpedo (an ASCII period) close in on its target though. So here’s Trek 24, where you must destroy all the Klingons before they destroy the Enterprise.

I wanted to take a stab at a Pong-era arcade game. (I can remember playing video games that were in black and white at the first arcade I ever went to.) I wasn’t sure what I could fit in 24 lines, but started over-ambitiously with a platformer – it instead evolved into a game of avoiding a sentry robot as it moves more and more quickly through level after level. The game supports 16 levels of accelerating velocity, almost each a different color, before restarting at the slower pace: SentryBot 24.

I’ve been interested in writing my own roguelike for a while now, but that always seemed too ambitious. Limiting it to 24 lines however – now there was a project I’d be able to actually finish. So, yes, I implemented a roguelike with 20 monsters and 18 items: in 24 lines of code! Roguelike 24.

When Was8bit started this challenge, I didn’t think you could do much with 24 lines. Perhaps some guessing games or simple simulations. But, in fact, you can accomplish a lot in 24 lines. Beyond the competition, the approach is useful to rapidly prototype an idea. I can see using it again for prototyping games, game subsystems, and levels, too. If you’re writing a new dungeon crawler, whether a card game, board game, or video game, why not prototype it as an ASCII roguelike first? A microgame is a great way to see if the foundation of a game design provides a sense of engaging play.

Tips for Writing Microgames

Interested in writing your own 24-line programs? Some tips I’ve discovered.

Push complexity from the game to the documentation. The list of monsters and objects in Roguelike 24 is close to 40 lines alone! With any game, much of the complexity and verisimilitude is in the player’s mind anyway, not the game itself. Hence the continued niche appeal of pixel games, interactive fiction, and ASCII roguelikes. And Dungeon & Dragons and Pathfinder games mainly take place in players’ imaginations.

Oversimplify the game design to concentrate on essentials. In my 4 In A Row, you can’t win diagonally – too hard to implement given the limit on the number of lines of code. In Roguelike 24, the strength of a monster is its ASCII code minus 64: Ant, 1; Bat, 2; Dwarf, 4; Zombie King, 26. I curated the list of monsters around this property (no D for Dragon and G for Giant in this scheme, for instance) and made it a Zombie King and not a Zombie. In Trek 24, instead of having Klingons detected when going to a new region, I have them show up in the region you just left—much simpler to code! Instead of detecting collisions, presumably the Klingons warp out of the way when you try to ram them. And so on.

Create the world as the player moves. When lines of code aren’t the constraint, it’s typically better to initialize the world and then store the state of the world (e.g., whether a room has been explored; see my Colossal Cave Adventure 101 for an example). In a microgame, create each quadrant of the galaxy or cell of the dungeon as it is explored.

Oversimplify data structures. Typically, clear data structures produce code that is easier to read and maintain. But that’s a luxury when you have 24 lines. Instead of having a two-dimensional array to represent a grid in Trek 24, I used a one-dimensional array. Couldn’t afford two FOR loops to display a matrix, plus two lines per move (e.g., X = X+1 and Y = Y+1 for Mark 8). I also made the arrays larger than necessary to eliminate the need for bounds checking. For Roguelike 24, I gave up on the array altogether and just manipulated a single string that represented the dungeon grid and used a fixed-width font to print it across multiple lines without line breaks.

Use strings for lists and sets. But don’t tell Mr. Ryden, my high-school computer-science teacher. Since 8-bit BASICs lack user defined types, use strings liberally. If you are using another language, user defined types probably would take too many lines of code, anyway. In Roguelike 24, new items are appended to INV$ to track inventory, and INSTR is used to determine if an item is in the inventory when trying to use it. Easy since items have a one-character code but I’ve used CSV (Comma-Separated Values) in other programs: e.g., “,ANT,BAT,GIANT,”, with the series string beginning and ending with a comma and with INSTR(MONSTERS$, “,”+M$+”,”) so that substrings aren’t false positives (e.g., so “ANT” isn’t found in “GIANT”).

Forget AIs to instead rely on random behavior—or no behavior. A game like Atari 2600’s Adventure implemented simple behaviorism through Finite State Machines for the dragons and the bat. But even such a simple model is too much code for this challenge. In Trek 24, Klingons never move. In 4 In A Row, the computer plays in the column you just played to or one of its neighboring columns at random. (Which is technically a tiny AI: the first version just selected a column at random, which was even easier to beat; now the game usually at least makes you work a little bit for the win.)

Specific Programming Tips

Writing to an arbitrary line limit does obfuscate the code in places, as a side effect. Here’s some tips for staying within 24 lines, readability aside.

Collapse multiline IF statements or refactor. For instance:

IF X=0 THEN
Y=1
Z=2
ENDIF

…becomes…

IF X=0 THEN Y=1
IF X=0 THEN Z=2

Or perhaps you can use SGN(). For instance, 10*SGN(PLAYER-ENEMY) gives a 10-point penalty if the player is weaker, a 10-point bonus if stronger, and has no effect if the player and enemy are equal. Better for minimizing cyclomatic complexity.

GOTO considered helpful. Helpful for saving lines, that is. In “Micro Game – Frog”, Was8bit adopts an old assembly-language technique of overincrementing so that one routine can fall through to the next (math is faster than jumps and saves code):

DORIGHT:
  PX=PX+16
DOLEFT:
  PX=PX-8
  LAYER 1
  CLS
  CIRCLE PX+4,60,4 PAINT

Offset values from 0 instead of initializing. For instance, instead of:

SHIELDS = 100
SHIELDS = SHIELDS-10
PRINT "SHIELDS AT " + SHIELDS
IF SHIELDS <= 0 THEN PRINT “YOU LOSE”

…use…

SHIELDS = SHIELDS-10
PRINT "SHIELDS AT " + (SHIELDS+100)
IF SHIELDS <= -100 THEN PRINT “YOU LOSE”

Move initialization FOR loops into the game. Part of the general philosophy of building the world as play goes on rather than procedurally generating it up front includes initialization, not just discovery.

FOR X= 1 TO 10
  A(X) = RND*10
NEXT X
…
FOR X =1 TO 10
  PRINT A(X);
NEXT X

…saves two lines when done this way…

FOR X=1 TO 10
  IF A(X)=0 THEN A(X) = RND*10
  PRINT X(A);
NEXT X

Tolerate errors. You can’t error check all user input and all game conditions. In 4 In A Row, playing your checker to a full column just causes you to lose your turn rather than prompting you for a different column selection. (This might be a nice equalizer against the AI, in this case, but the AI actually does this far more often than a human player would.)

To force user input into a range, use this formula:

MAX(1, MIN(M, 8))

If the player enters a number lower than the minimum, it is treated like the minimum (in this case, 1). If more than the maximum, it is treated like the maximum (in this case, 8).

Use strings as lookup functions instead of having arrays or READ/DATA statements. Here is an example for Trek 24 for navigation from Mark 1 to Mark 8:

MID$(" 1-7-8-9-1 7 8 9", CMD*2-1, 2)

This is analogous to the Excel function CHOOSE; e.g., CHOOSE(CMD, 1, -7, -8, -9, -1, 7, 8, 9). Here’s the lookup for movement distance in Roguelike 24:

MID$( "+0-8+8+1-112",INSTR("NSEWT",CMD$)*2+1,2)

North is -8, South is +8, East is +1, West is -1, and Teleport is +12. If no command is found, +0 is returned.

In Roguelike 24, INSTR is used to find the relative strength of each weapon and also to find the bane (from a 26-character string) that corresponds to the monster being fought.

Call to Code

So you’ve been wanting to design a game but are overwhelmed by the size of the project? Try a microgame instead! If you have an iOS device, download LowRes Coder or try the language of your choice in a browser (Try It Online).

Happy coding!

Colossal Cave Adventure 101

Colossal Cave 101

When my 11-year old wanted to learn to program last spring, I got him a book on writing games in JavaScript. It had the source code for four games, but my son found it difficult. Four games?! A far cry from the 101 BASIC Computer Games that was my go-to (ahem) when I was his age, programming a TRS-80 Model I.

So I looked at a number of different BASIC implementations for his iPod Touch, eventually landing on LowRes Coder, which turns an iOS device into an 8-bit microcomputer circa 1979. I jokingly describe it as “turning your iPhone into an Apple II”, but – having programmed an Apple II – I can tell you that LowRes Coder is a lot more powerful. While it lacks functions and procedures (GOSUB rules the day here), it has a rich variety of sprite commands. And, thanks to the Internet, people can easily share their BASIC programs with one another without having to retype them or, shudder, load them from cassette. The point of it being “low resolution” is that no one has to worry about the quality of their art: the fun is in making the games.

And my son found, as I did before him, that it is easy to write BASIC programs. Loving math, he’s written a number of math programs, and of course some graphical experiments, and an adventure game.

The sample adventure program that he based his game on, though, was menu-based, and his aspirations went beyond that. So a few weekends ago I ported Colossal Cave to LowRes Coder, streamlining it for BASIC and adding random maps. While it has a two-word parser system, it’s driven by INKEY$, so selecting the first letter shows the entire word: no guess-the-verb problems here.

The app is free if you’re just going to play other people’s games. Here’s Colossal Cave Adventure 101. (And here’s my card game inspired by Colossal Cave.)