Atari BASIC programming
This resource includes primary and/or secondary research. Learn more about original research at Wikiversity. |
This article by Dan Polansky intends to give an impression of Atari BASIC for 8-bit Atari computers, including exercises and examples. One may try out the examples in an Atari emulator such as Atari800 or Altirra (with BASIC ROM) and appreciate the speed of modern computers. One can have more fun by trying the exercises without reading the solutions. One purpose is to get an idea of what kinds of simple programs kids in the 1980ies could have been writing. Another purpose is to give an idea to beginners how programming close to the hardware looks like, e.g. by manipulating the video memory.
Identification and hardware
editAtari BASIC refers here to versions originally distributed in ROM cartridges and later in ROM chips. The target for testing most content on this page is the 'final "C" version in late-model XLs and the XE series.' The version can be determined by PRINT PEEK (43234).[1]
The target hardware may include the 1979 Atari 400 and 800 computers (8 KB or 16 KB) and 1983 600 XL (16 KB) but is above all the 1983 Atari 800 XL (64 KB). There is not much difference between Atari 800 XL and the later XE machines. Working hypothesis: some graphical modes are only available on the XL and XE machines.
Exercise statements
edit1) Print numbers from 1 to 10.
2) Calculate the factorial of n input by the user and print it.
3) Plot a large cross made from two diagonal lines.
4) Plot an ellipse using sine and cosine and plotting the individual pixels.
5) Cover the screen in random-color pixels in various graphical modes.
6) Plot the Mandelbrot set in a 2-color, 4-color or 16-color mode.
7) Other exercises can be inferred from section headings.
Elements
editGetting started
editPrograms have to be entered with line numbers. One can see the program listing by typing "LIST" or "L.". One can list a particular line number e.g. by typing "LIST 30". Once one is ready to run the program, one enters "RUN". To enter a line, one types e.g. "10 PRINT 3*7". To erase a line, one types just its line number, e.g. "10".
One can also enter BASIC commands interactively, e.g. by typing "PRINT 3*7".
To erase the current program, one can type NEW.
The Atari emulator may have a "Paste" option in its context menu that types text content of the clipboard.
A paused program can be resumed using CONT.
Variable names cannot not start with a builtin keyword name. Thus, entering LETTER=1 results in TER being set to 1 since the statement is interpreted a LET TER=1. Similarly, FORMULA=1 results in error: it is interpreted as FOR MULA=1. Underscores are disallowed in variable names.
Text output
edit10 A=1:B=2:C=3:D=4 20 PRINT A,B,C,D:REM COLUMNS WITH LARGE SEPARATION 30 PRINT A;B;C;D:REM NO SEPARATION 40 PRINT A;" ";B;" ";C;" ";D 50 PRINT A;:PRINT B:REM NO NEWLINE AFTER A
Conditionals
editThere is IF conditional. There is no block grouping (BEGIN, END or {, }); at least, multiple statements can be made on the same line of the IF conditional. To achieve arbitrarily large blocks guarded by IF, one uses GOTO.
10 IF 5>4 THEN PRINT 1:PRINT 2 20 INPUT A:IF A<=4 THEN GOTO 40 30 PRINT "A IS > 4" 40 INPUT A:IF A>=2 THEN 60:REM IMPLICIT GOTO 50 PRINT "A IS < 2" 60 END
There is no ELSE. To achieve if-else, one can either use GOTO or one can repeat the condition, e.g. like this:
10 INPUT X 20 IF X>=10 THEN PRINT "X>=10" 30 IF X<10 THEN PRINT "X<10"
There is no SWITCH command.
Loops
editA for loop:
10 FOR I=1 TO 10 20 PRINT I 30 NEXT I
An early exit from the loop ("break"):
10 FOR I=1 TO 10 20 PRINT I 30 IF I=5 THEN I=10 40 NEXT I
There is no while loop. One can use GOTO instead:
10 I=1 20 IF I>10 THEN GOTO 60 30 PRINT I 40 I=I+1 50 GOTO 20 60 END
One can also emulate a do-while (repeat until) loop using GOTO:
10 I=1 20 PRINT I 30 I=I+1 40 IF I<=10 THEN GOTO 20
Arrays
editOne can dimension and use a one-dimensional array of Atari BASIC numbers.
10 DIM A(10) 20 FOR I=1 TO 10:A(I)=I:NEXT I 30 FOR I=1 TO 10:PRINT A(I):NEXT I
One can also have two-dimensional arrays, but not three-dimensional ones:
10 DIM A(10,10) 20 FOR I=1 TO 10 30 FOR J=1 TO 10:A(I,J)=I*J:NEXT J 40 NEXT I 50 FOR I=1 TO 10 60 FOR J=1 TO 10:PRINT A(I,J);";";:NEXT J 70 PRINT 80 NEXT I
One can emulate any-dimensional array by using a one-dimensional array and calculating the physical index from the logical indices.
String arrays are impossible except as being emulated by using a single string and interpreting it as sequence of blocks.
Strings, like arrays, need to be dimensioned, and can be thought of as 8-bit character arrays. They can be actually used as arrays of bytes/octets: to store a byte, one would use CHR$, and to retrieve a byte, one would use ASC. The benefit is that this way, one item take one bytes, whereas an Atari BASIC number takes 6 bytes.
Strings
editStrings have to be dimensioned for size before use. Their use is as follows:
10 DIM S$(5) 20 S$="HEY":PRINT S$ 30 S$="HELLO":PRINT S$ 40 S$(2,2)="A":PRINT S$ 50 FOR I=1 TO LEN(S$):PRINT S$(I,I);:NEXT I:? 60 S$="HELLO1":REM TRUNCATE 1 70 PRINT CHR$(65):REM A 80 PRINT ASC("A") 90 DIM H$(11) 100 H$="HELLO" 110 H$(LEN(H$)+1)=" WORLD":REM CONCATENATE 120 PRINT H$
Strings can be used as byte arrays, via ASC and CHR$. The syntax is cumbersome but if one uses a numerical array to store byte values, one value occupies 6 bytes. Setting the byte value to 65 and then incrementing it:
10 DIM S$(5) 20 FOR I=1 TO 5: S$(I,I)=CHR$(65): NEXT I 30 FOR I=1 TO 5: S$(I,I)=CHR$(ASC(S$(I,I))+1): NEXT I 40 PRINT S$
We can also use strings as byte arrays via ADR, PEEK and POKE:
10 DIM S$(5) 15 S$(5)="A":SA=ADR(S$) 20 FOR I=1 TO 5: POKE SA+I-1, 65: NEXT I 30 FOR I=1 TO 5: POKE SA+I-1, PEEK(SA+I-1)+1: NEXT I 40 PRINT S$
ADR and other techniques from above are also used to store machine code in a string, as per Calling assembly section.
While variables (including strings) keep their values when the program ends, the variable locations in memory are not guaranteed to be remain intact. Thus, storing a string address into a variable SA during a run of a program and then trying to access the string via that variable after the program ended may lead to a bad surprise.
Floating point numbers
editThe numbers Atari BASIC works with are floating point numbers; one cannot declare a variable to be an integer. They are 6-byte floating point numbers. There is 1 bit for sign, 7 bits for exponent and 5 bytes for binary-coded decimal (BCD) mantissa, representing 10 decimal digits. The 7-bit exponent indicates not the power of 10 but rather the power of 100. A quick and inconclusive experiment suggests that the largest number is 9.99999999E97, the smallest number is -9.99999999E97 and the smallest positive number is 1.0E-98.
The floating point output and entry make it seem that there are only 9 decimal digits rather than 10. Thus, when one enters "A=0.3333333333" (10 threes), one only really enters 9 threes. However, by entering "A=1/3", one makes use of all the 10 decimal digits. One can enter all 10 decimal digits (arbitrary ones) by a trick: "A=0.333333333+3E-10", and one can then verify success by another trick: "PRINT A-0.3". Further calculation often does make use of all the 10 digits, as verifiable e.g. thus: "A=1/3:A=A*2:PRINT A-0.6". Full 10 digits seem often used by trigonometric functions, e.g. "A=SIN(0.5):PRINT A-0.4". Similarly, full 10 digits are used here: "PRINT ATN(1)-0.7". However, the use of only 9 digits is revealed in "PRINT ATN(10)-1", possibly because ATN(10) is 1.47... and the uppermost two digits in the internal representation are "01". Here, the uppermost digit cannot be "1" since the exponent as internally represented cannot represent powers of 10, only powers of 100. Thus, given a floating point number with non-zero leading digit in the internal representation and thus with full 10 decimal digits, one loses 1 digit of precision by either multiplying it or dividing it by 10. Thus, 1/3 is 10-precise, 10/3 is 9-precise, 100/3 is 10-precise, 1000/3 is 9-precise, etc, which one can verify thus: "? 1/3-0.3:? 10/3-3:? 100/3-30:? 1000/3-300:? 10000/3-3000:? 100000/3-30000".
The pseudo random number generator often achieves the full 10-digit precision, as per this: "FOR I=1 TO 10:A=RND(0):PRINT A:PRINT A-INT(A*10)/10:NEXT I".
Other tests:
10 MAXNUM=9.99999999E97 20 PRINT MAXNUM 30 MINNUM=-9.99999999E97 40 PRINT MINNUM 50 PRINT MAXNUM+MINNUM 60 SMALLPOSNUM=1.0E-98:REM PERHAPS NOT SMALLEST 70 PRINT SMALLPOSNUM 80 THIRD=1/3 90 PRINT THIRD 100 IF THIRD=0.333333333 THEN GOTO 100 110 PRINT "THIRD DIFFERS WITH DELTA:", THIRD-0.333333333
The following tests suggests that 1000000000 (1E9) is the largest integer contiguous with other integers with units precisely represented (and thus, 29-bit unsigned integers are fully covered):
10 A=999999999 20 PRINT A 30 A=A+1 40 IF A<1000000010 THEN GOTO 20
However, the above terminates after 10 iterations while outputting 10000000000. This puzzle is due to the fact that the output pretends the precision is never more than 9 decimal digits.
One more test:
10 FOR I=1 TO 100 20 J=1E9+I 30 K=J-1E9 40 PRINT I,K,J 50 NEXT I
The above outputs K with the same precision as I, but the J output has the final digit replaced with zero, suggesting that the representation during calculation is different from the one for output, and also possibly different from the representation used for tokenization (for storage as a literal in the program).
The internal representation of floating point numbers can be investigated using the following:
10 INPUT A 20 VB=PEEK(134)+256*PEEK(135):REM VARIABLE BASE 30 PRINT PEEK(VB+2);" ";PEEK(VB+3);" ";PEEK(VB+4);" ";PEEK(VB+5);" ";PEEK(VB+6);" ";PEEK(VB+7)
We may explore the internal representation in hexadecimal, using more code:
10 A=0:VB=PEEK(134)+256*PEEK(135):REM VARIABLE BASE 20 A=1:GOSUB 100:A=10:GOSUB 100:A=100:GOSUB 100 30 A=-1:GOSUB 100 40 A=9.99999999E97:GOSUB 100:A=-9.99999999E97:GOSUB 100 50 A=1E-98:GOSUB 100:A=1/3:GOSUB 100:A=1/7:GOSUB 100 60 END 100 REM ---- OUTPUT INTERNAL HEX OF A 110 PRINT ":";A:PRINT ">"; 120 FOR OFF=2 TO 7 130 VL=PEEK(VB+OFF) 140 U=INT(VL/16):L=VL-U*16 150 I=U:GOSUB 200:I=L:GOSUB 200 160 NEXT OFF 170 PRINT:RETURN 200 IF I<10 THEN PRINT I;:RETURN 210 PRINT CHR$(65+I-10);:RETURN
The output confirms the ideas we have about the internal representation:
:1 >400100000000 :10 >401000000000 :100 >410100000000 :-1 >C00100000000 :9.99999999E+97 >709999999990 :-9.99999999E+97 >F09999999990 :1E-98 >0F0100000000 :0.333333333 >3F3333333333 :0.142857142 >3F1428571428
Link:
- Inside Atari BASIC by Larry Isaacs, atariarchives.org
- Why did 8-bit Basic use 40-bit floating point?, retrocomputing.stackexchange.com -- the title of the question is misleading since Atari BASIC uses 48-bit floating point
- De Re Atari, archive.org -- section Floating Point Package 8-45 (its claim that the end of dynamic range is 1E98 appears incorrect)
Mathematics
editMathematical operators include +, -, *, / and ^.
Mathematical functions include abs, sin, con, atn, etc.
The exponentiation operator ^ has a limitation or glitch: it is imprecise even for relatively low integral operands. Thus, e.g. 10^9 yields 999999999, one off. One can get a precise value via multiplication in a loop.
Graphics modes
editIn the plotting examples below, we refer to graphics modes via GRAPHICS command. A table of those is in the link. For a start, we need to know:
- mode 0 is the default, hi-res text/character mode
- mode 7 is a low-res four-color pixel bitmap mode with split text window; this is the resolution of Draconus although Draconus (from what I recall) uses character mode rather than pixel bitmap mode
- mode 8 is a hi-res two-color pixel bitmap mode with split text window
- mode 8+16 is a variant of mode 8 without a split window, 320x192 pixels
Links:
- Atari Graphics and Arcade Game Design-Chapter 1, atariarchives.org
Player-missile graphics
editPlayer-missing graphics (called sprites on some computers) can be controlled only via low-level access, peeks and pokes. One can use string tricks, by which one cleverly makes sure a string to contain the PMG buffer is aligned at a boundary, which is required of the buffer. Moreover, copying strings is rather fast, unlike copying bytes one by one in a for loop; and copying blocks of bytes is required to change the vertical position of the PMG objects.
Links:
- Atari Player-Missile Graphics in BASIC by Philip C. Seyer, 1984, atariarchives.org
- Player-Missile Graphics in The Creative Atari, 1983, atariarchives.org
Reporting joystick state/events
edit10 PRINT STICK(0), STRIG(0) 20 GOTO 10
Clearing the screen
editThere is no CLS. One can PRINT CHR$(125) instead.
Abbreviations
editAbbreviations include:
- "?" for PRINT.
- L. for LIST
- GR. for GRAPHICS
- SE. for SETCOLOR
- Etc.
Links:
- Atari BASIC Quick Reference Guide, atariwiki.org
Memory map
editMemory map is useful to know the effects of various PEEKs and POKEs. See the link.
Links:
- Mapping The Atari-Memory Map, atariarchives.org
Measuring time
editMeasuring time is useful e.g. for benchmarking.
The link below suggests the following (however, one of the responses warns against race conditions):
TIME = INT((PEEK(18) * 65536 + PEEK(19) * 256 + PEEK(20))/60)
Above, the division by 60 is for NTSC; it should be 50 for PAL.
Links:
- benchmarking - How can you measure time using BASIC on Atari XL computers?, retrocomputing.stackexchange.com
Manipulating video memory
editThere is value in being able to directly access video memory (video buffer) using POKE and PEEK. For a character mode, one can not only write (with no cursor effect) but also read the character at a given position on screen. For a graphical mode, one can read the pixel rather than only changing it via PLOT.
Two methods of determining the beginning of the video memory can be found in non-authoritative sources:
10 DL=PEEK(560)+PEEK(561)*256 20 VB1=PEEK(DL+4)+PEEK(DL+5)*256 30 VB2=PEEK(88)+PEEK(89)*256
One cannot generally know a fixed address for a given graphical mode, in part since different Atari machines have different available memory, and for other reasons.
The following inconclusive test suggests the two methods are equivalent under some circumstances:
10 DIM VB1(10),VB2(10) 20 FOR MN=0 TO 8 30 GRAPHICS MN 40 DL=PEEK(560)+PEEK(561)*256 50 VB1(MN)=PEEK(DL+4)+PEEK(DL+5)*256 60 VB2(MN)=PEEK(88)+PEEK(89)*256 70 NEXT MN 80 GRAPHICS 0 90 FOR MN=0 TO 8 100 PRINT "MN,VB1,VB2:";MN;",";VB1(MN);",";VB2(MN) 110 NEXT MN
Links:
- atari 800 - where does address start for graphics 8 mode, forums.atariage.com
Calling assembly
editOne can call assembly/machine code using USR.
Two places where to store the machine code:
- Page 6: starts at 1536, at most 256 bytes (a page)
- String, declared via DIM, with more than 256 bytes possible
Two manners of machine code entry:
- DATA statement
- String literal
Interaction/interfacing between the calling BASIC code and the machine code:
- Stack contains the number of arguments and then two bytes per argument containing the integer part of the argument value. It is to be found out how and whether a string argument can be passed.
- The USR return value is at addresses 212 and 213.
To obtain the machine code from assembly, one can use e.g. the online assembler at masswerk.at. However, it outputs hexadecimal codes, whereas the literals used with DATA command are decimal.
As an example, we implement 8-bit bitwise OR, to fill the gap in the BASIC command/function set:
ASM ----- PLA ;arg count; discard PLA ;arg1 upper byte; discard PLA ;arg1 lower byte STA 212; lower byte of USR return value PLA ;arg2 upper byte; discard PLA ;arg2 lower byte ORA 212 STA 212 LDA #0 STA 213 ;clear the upper byte of ret val RTS ASM HEX ----- 0000: 68 68 68 85 D4 68 68 05 0008: D4 85 D4 A9 00 85 D5 60 ASM DEC ----- 0000: 104 104 104 133 212 104 104 5 0008: 212 133 212 169 00 133 213 96 BASIC WITH ASM AT PAGE 6 ------------------------ 10 X=0:RESTORE 40 20 READ D:IF D=-1 THEN GOTO 100 30 POKE 1536+X,D:X=X+1:GOTO 20 40 DATA 104, 104, 104, 133, 212, 104, 104, 5 50 DATA 212, 133, 212, 169, 00, 133, 213, 96, -1 100 INPUT A, B 110 RES=USR(1536, A, B) 120 PRINT RES BASIC WITH ASM AT A STRING -------------------------- 5 DIM ORASM$(20) 10 X=1:RESTORE 40 20 READ D:IF D=-1 THEN GOTO 100 30 ORASM$(X)=CHR$(D):X=X+1:GOTO 20 40 DATA 104, 104, 104, 133, 212, 104, 104, 5 50 DATA 212, 133, 212, 169, 00, 133, 213, 96, -1 100 INPUT A, B 110 RES=USR(ADR(ORASM$), A, B) 120 PRINT RES
A related page: Learning 6502 assembly.
Link:
- Page 6 - Issue 11 - What is USR?, page6.org
- Call Assembly Language Code from Your BASIC Program (10-15 mins), atariprojects.org -- has decimal values for 6502 assembly instructions
- virtual 6502 Assembler, masswerk.at
- 6502 Instruction Set, masswerk.at
- Easy 6502 by skilldrick, skilldrick.github.io
- Part 3 of 11 -- Simple Assembly for Atari BASIC - kenjennings' Blog, forums.atariage.com
Limitations
editLimitations of Atari BASIC include the following:
- Very limited procedural programming (with GOSUB and RETURN): no named procedures, no argument declaration and no automatic storage of local variables on the stack.
- No separation of integer arithmetic from floating-point arithmetic. The consequence is a major slowdown of what could otherwise be integer operations.
- No while loop. One can use GOTO instead.
- No blocks (like BEGIN and END). One can use GOTO instead.
- No bitwise operators.
- No taking of address of a variable except for a string variable.
- No hexadecimal literals.
- No commands for player-missile graphics.
- Almost no plotting commands: only PLOT and DRAWTO.
- No CLS (very minor limitation if one at all).
Exercises
editCalculating factorial
edit10 INPUT N 20 F=1 30 FOR I=1 TO N 40 F=F*I 50 NEXT I 60 PRINT F
Plotting diagonal lines
edit10 GRAPHICS 8 11 COLOR 1 20 FOR X=0 TO 150 30 PLOT X,X 40 PLOT 150-X, X 50 NEXT X
Depends on PLOT for plotting individual pixels. To switch back to the default graphics mode for comfortable source code listing, one may type "GRAPHICS 0".
Doing the same by drawing lines using DRAWTO:
10 GRAPHICS 8 20 COLOR 1 30 PLOT 0,0 40 DRAWTO 150,150 50 PLOT 150,0 60 DRAWTO 0,150
Plotting an ellipse
edit10 GRAPHICS 8 20 COLOR 1 30 RDS=75 40 FOR X=0 TO 2*3.141592 STEP 0.02 50 PLOT 2*RDS+2*RDS*SIN(X),RDS+RDS*COS(X) 60 NEXT X
The above is very slow.
Random numbers
editThe function RND outputs floating-point numbers in the range of 0 to 1, excluding 1. We can output random integers using INT function (here we output random integers from 0 to 9 including):
10 FOR I=0 to 10 20 PRINT INT(10*RND(1)) 30 NEXT I
We can obtain random integers from 0 to 255 from address 53770. The following plots the counts of generated values, allowing a partial verification of the generator by means of visual inspection.
10 REPEATCOUNT=1000000 20 DIM VALCOUNT(256) 30 GRAPHICS 8: COLOR 1 40 FOR I=1 TO 256: VALCOUNT(I)=0: NEXT I 50 FOR I=1 TO REPEATCOUNT 60 R=PEEK (53770): PRINT "RND: ";R 70 VALCOUNT(R+1)=VALCOUNT(R+1)+1 80 IF VALCOUNT(R+1)>160 THEN END:REM SCREEN LIMIT 90 PLOT R, VALCOUNT(R+1)-1 100 NEXT I
We can make the above frequency investigation for the Mandelbrot map-based pseudorandom generator (for more of which see section Plotting random pixels):
10 REPEATCOUNT=1000000 20 DIM VALCOUNT(256) 30 GRAPHICS 8: COLOR 1 35 X=0.1 40 FOR I=1 TO 256: VALCOUNT(I)=0: NEXT I 50 FOR I=1 TO REPEATCOUNT 55 GOSUB 200:REM RND 60 R=INT(256*X): PRINT "RND: ";R 70 VALCOUNT(R+1)=VALCOUNT(R+1)+1 80 IF VALCOUNT(R+1)>160 THEN END:REM SCREEN LIMIT 90 PLOT R, VALCOUNT(R+1)-1 100 NEXT I 110 END 200 REM NEXT RND 205 X=X*4-2.0 210 X=X*X-2.0:REM FROM [-2, 2] to [-2, 2] 215 X=X/4+0.5 216 IF X=1 THEN X=0.999999999 260 RETURN
Links:
- The Beginner's Page: The Random Function, atarimagazines.com
Plotting random pixels
editPlotting random pixels in 2-color high-resolution mode (with text window):
10 GRAPHICS 8 20 COLOR 1 30 FOR X=0 TO 319 40 FOR Y=0 TO 159 50 C=INT(2*RND(1)) 60 COLOR C 70 PLOT X,Y 80 NEXT Y 90 NEXT X
Plotting random pixels in 4-color low-resolution (2x2-sized pixel) mode (with text window):
10 GRAPHICS 7 20 COLOR 1 30 FOR X=0 TO 159 40 FOR Y=0 TO 79 50 C=INT(4*RND(1)) 60 COLOR C 70 PLOT X,Y 80 NEXT Y 90 NEXT X
Plotting random pixels in 16-color low-resolution mode (4x1-sized pixel):
10 GRAPHICS 9 20 COLOR 1 30 FOR X=0 TO 79 40 FOR Y=0 TO 191 50 C=INT(16*RND(1)) 60 COLOR C 70 PLOT X,Y 80 NEXT Y 90 NEXT X
Plotting random pixels by low-level access to video memory (not particularly fast either):
10 GRAPHICS 8 20 P=PEEK(560)+PEEK(561)*256 30 VB=PEEK(P+4)+PEEK(P+5)*256:REM VIDEOBASE 40 FOR I=0 TO 320/8*160-1 50 POKE VB+I, PEEK(53770) 60 NEXT I
Plotting random pixel by low-level access, using not the RND but rather custom low-quality pseudorandom generator based on the Mandelbrot map f(x) = x**2 - 2. The result looks sort of random, but with suspectly long streaks/blocks of pixels, which probably correspond to the attractive power of x = 2 for the f(x). We rescale [-2, 2] to [0, 1] to make this work; rescaled, the attracting point x=1 shows it power, e.g. in the following sequence:
- 0.999753695
- 0.999015022
- 0.99606397
- 0.984317847
The properties of the Mandelbrot map are investigated in the article Mandelbrot set along the real axis and the orbits.
10 GRAPHICS 8 20 P=PEEK(560)+PEEK(561)*256 30 VB=PEEK(P+4)+PEEK(P+5)*256:REM VIDEOBASE 35 X=0.1 40 FOR I=0 TO 320/8*160-1 45 BYTE=INT(256*X) 50 POKE VB+I,BYTE 55 GOSUB 100 60 NEXT I 70 END 100 REM NEXT RND 105 X=X*4-2.0 110 X=X*X-2.0:REM FROM [-2, 2] to [-2, 2] 115 X=X/4+0.5 116 IF X=1 THEN X=0.999999999 155 PRINT X;","; 160 RETURN
Outputting Atari ASCII characters
editOutputting Atari ASCII sequence except for special CHR$ codes, which are replaced with underscore ("_"):
10 DIM S$(256) 20 FOR I=0 TO 255 30 S$(I+1)=CHR$(I) 40 NEXT I 60 S$(27+1)="_" 61 S$(28+1)="_" 62 S$(29+1)="_" 63 S$(30+1)="_" 64 S$(31+1)="_" 80 S$(125+1)="_" 90 S$(155+1)="_" 100 S$(253+1)="_" 101 S$(254+1)="_" 102 S$(255+1)="_" 110 PRINT S$
Outputting them one character per line, with the ASCII code:
10 DIM S$(256) 20 FOR I=0 TO 255 30 S$(I+1)=CHR$(I) 40 NEXT I 60 S$(27+1)="_" 61 S$(28+1)="_" 62 S$(29+1)="_" 63 S$(30+1)="_" 64 S$(31+1)="_" 80 S$(125+1)="_" 90 S$(155+1)="_" 100 S$(253+1)="_" 101 S$(254+1)="_" 102 S$(255+1)="_" 110 FOR I=0 TO 255 120 PRINT I, S$(I+1,I+1) 130 NEXT I
Links:
- ATASCII, wikipedia.org
- ATASCII to Unicode Mapping, kreativekorp.com
- The Beginners Page: Printing And Asking, atarimagazines.com
Plotting Mandelbrot set
editAn adaptation of AWK code from rosettacode.org for the 2-color mode 8:
10 GRAPHICS 8 20 XSIZE=320:YSIZE=160 30 ITERMAX=10 40 MINIM=-1.0:MAXIM=1.0:MINRE=-2.0:MAXRE=1.0 50 STEPX=(MAXRE-MINRE)/XSIZE:STEPY=(MAXIM-MINIM)/YSIZE 60 FOR Y=0 TO YSIZE-1 70 CIM=MINIM+STEPY*Y 80 FOR X=0 TO XSIZE-1 90 CRE=MINRE+STEPX*X 100 ITCOUNT=0 110 RE=0:IM=0 120 RESQ=RE*RE:IMSQ=IM*IM 130 IF RESQ+IMSQ > 4 THEN GOTO 170 140 ITCOUNT=ITCOUNT+1 150 IM=2*RE*IM+CIM:RE=RESQ-IMSQ+CRE 160 IF ITCOUNT<ITERMAX THEN GOTO 120 170 ICMOD = ITCOUNT-INT(ITCOUNT/2)*2 180 COLOR ICMOD 190 PLOT X,Y 200 NEXT X 210 NEXT Y
A modification of the above for the 4-color mode 7:
10 GRAPHICS 7 15 SETCOLOR 4,0,0:SETCOLOR 0,0,5:SETCOLOR 1,0,10:SETCOLOR 2,0,15 20 XSIZE=160:YSIZE=80 30 ITERMAX=16 40 MINIM=-1:MAXIM=1:MINRE=-2:MAXRE=1 50 STEPX=(MAXRE-MINRE)/XSIZE:STEPY=(MAXIM-MINIM)/YSIZE 60 FOR Y=0 TO YSIZE-1 70 CIM=MINIM+STEPY*Y 80 FOR X=0 TO XSIZE-1 90 CRE=MINRE+STEPX*X 100 ITCOUNT=0 110 RE=0:IM=0 120 RESQ=RE*RE:IMSQ=IM*IM 130 IF RESQ+IMSQ>4 THEN GOTO 170 140 ITCOUNT=ITCOUNT+1 150 IM=2*RE*IM+CIM:RE=RESQ-IMSQ+CRE 160 IF ITCOUNT<ITERMAX THEN GOTO 120 170 ICMOD=ITCOUNT-INT(ITCOUNT/4)*4 180 COLOR ICMOD 190 PLOT X,Y 200 NEXT X 210 NEXT Y
A modification of the above for the 16-color mode 9:
10 GRAPHICS 9 20 XSIZE=80:YSIZE=192 30 ITERMAX=16 40 MINIM=-1.0:MAXIM=1.0:MINRE=-2.0:MAXRE=1.0 50 STEPX=(MAXRE-MINRE)/XSIZE:STEPY=(MAXIM-MINIM)/YSIZE 60 FOR Y=0 TO YSIZE-1 70 CIM=MINIM+STEPY*Y 80 FOR X=0 TO XSIZE-1 90 CRE=MINRE+STEPX*X 100 ITCOUNT=0 110 RE=0:IM=0 120 RESQ=RE*RE:IMSQ=IM*IM 130 IF RESQ+IMSQ > 4 THEN GOTO 170 140 ITCOUNT=ITCOUNT+1 150 IM=2*RE*IM+CIM:RE=RESQ-IMSQ+CRE 160 IF ITCOUNT<ITERMAX THEN GOTO 120 170 ICMOD = ITCOUNT-INT(ITCOUNT/16)*16 180 COLOR ICMOD 190 PLOT X,Y 200 NEXT X 210 NEXT Y
Calculating modulo
editAtari BASIC has no built-in modulo operator. Thus:
10 INPUT X, Y 20 MOD = X - Y*INT(X/Y) 30 PRINT X;" MOD ";Y;": ";MOD
Random walk
editPlot a random walk: let x start at 0 and in each successive step, let x either stay where it is, have 1 added to it or have 1 subtracted from it, where one of the 3 options is chosen at random.
10 GRAPHICS 8 20 COLOR 1 30 PLOT 0,0 40 DRAWTO 0,160 50 PLOT 0,80 60 DRAWTO 319,80 70 X=0 80 STEPNO=0 90 PLOT STEPNO,80-X 100 X=X+INT(3*RND(1))-1 110 STEPNO=STEPNO+1 120 IF STEPNO<320 THEN GOTO 90
A different kind of random walk in which the system starts at the middle of the screen, and then both of its X and Y coordinates are repeatedly subject to addition of one of -1, 0, or 1, chosen at random:
10 GRAPHICS 8 20 COLOR 1 30 X=160:Y=80 40 PLOT X,Y 50 X=X+INT(3*RND(1))-1 60 Y=Y+INT(3*RND(1))-1 70 GOTO 40
Plotting Mandelbrot map orbit diagram
editLet Mandelbrot map refer to x**2 + c. Plotting its orbit diagram/bifurcation diagram:
10 GRAPHICS 8 20 COLOR 1 30 FOR X=0 TO 320-1 40 C=X/320*2.25-2 50 Y=0 60 FOR IT=0 TO 20 70 YPL=80-Y*40 75 PLOT X,YPL 80 Y=Y*Y+C 90 NEXT IT 100 NEXT X
A variant of the above with initial iterations not plotted:
10 GRAPHICS 8 20 COLOR 1 30 FOR X=0 TO 320-1 40 C=X/320*2.25-2 50 Y=0 60 FOR IT=0 TO 40 70 YPL=80-Y*40 72 IF IT <= 20 THEN GOTO 80 75 PLOT X,YPL 80 Y=Y*Y+C 90 NEXT IT 100 NEXT X
Color registers
editColor registers are affected via SETCOLOR. The SETCOLOR parameters are color register number, hue and luminance.
Color register test for two-color text mode 0:
10 GRAPHICS 0 20 PRINT "HELLO" 30 SETCOLOR 0,2,8:REM NO IMPACT 40 SETCOLOR 1,7,0:REM TEXT FOREGROUND; HUE HAS NO IMPACT 50 SETCOLOR 2,0,15:REM TEXT BACKGROUND; SETS HUE FOR TXT FRG 60 SETCOLOR 3,3,8:REM NO IMPACT 70 SETCOLOR 4,1,8:REM OUTER BACKGROUND
Color register test for two-color pixel bitmap mode 8:
10 GRAPHICS 8 20 COLOR 0:PLOT 0,0:DRAWTO 319,0 30 COLOR 1:PLOT 0,1:DRAWTO 319,1 35 SETCOLOR 0,2,8:REM NO IMPACT 40 SETCOLOR 1,7,0:REM COLOR 1; HUE HAS NO IMPACT 50 SETCOLOR 2,0,15:REM COLOR 0; SETS HUE FOR COLOR 1 60 SETCOLOR 3,3,8:REM NO IMPACT 70 SETCOLOR 4,1,8:REM OUTER BACKGROUND
Color register test for four-color pixel bitmap mode 7:
10 GRAPHICS 7 20 COLOR 0:PLOT 0,0:DRAWTO 80-1,0 25 COLOR 1:PLOT 80,0:DRAWTO 160-1,0 30 COLOR 0:PLOT 0,1:DRAWTO 80-1,1 35 COLOR 1:PLOT 80,1:DRAWTO 160-1,1 40 COLOR 2:PLOT 0,2:DRAWTO 80-1,2 45 COLOR 3:PLOT 80,2:DRAWTO 160-1,2 50 COLOR 2:PLOT 0,3:DRAWTO 80-1,3 55 COLOR 3:PLOT 80,3:DRAWTO 160-1,3 60 SETCOLOR 0,0,5:REM COLOR 1 70 SETCOLOR 1,0,10:REM COLOR 2 80 SETCOLOR 2,0,15:REM COLOR 3 90 SETCOLOR 3,2,8:REM NO IMPACT 95 SETCOLOR 4,0,0:REM COLOR 0 AND OUTER BACKGROUND
Color register test for four-color pixel bitmap mode 7 with low-level access (POKEs and bit patters):
10 GRAPHICS 7 20 P=PEEK(560)+PEEK(561)*256 30 VB=PEEK(P+4)+PEEK(P+5)*256: REM VIDEOBASE 35 LB=320/8:REM LINE BYTES 40 FOR I=0 TO LB-1: POKE VB+I, 0: NEXT I 50 FOR I=LB TO 2*LB-1: POKE VB+I, 85: NEXT I 60 FOR I=2*LB TO 3*LB-1: POKE VB+I, 170: NEXT I 70 FOR I=3*LB TO 4*LB-1: POKE VB+I, 255: NEXT I 80 SETCOLOR 0,0,5:REM COLOR 1 90 SETCOLOR 1,0,10:REM COLOR 2 100 SETCOLOR 2,0,15:REM COLOR 3 110 SETCOLOR 3,2,8:REM NO IMPACT 120 SETCOLOR 4,0,0:REM COLOR 0 AND OUTER BACKGROUND
The above confirms the correspondence of the color index used by COLOR to bit patterns 0b00, 0b01, 0b10 and 0b11 in the video memory.
Links:
- Compute!'s Second Book of Atari Graphics, atariarchives.org
Low-level access plotting
editAssignment: plot diagonal lines using low-level bit-manipulation programming for plotting pixels in color 1 in mode 8, avoiding PLOT and DRAWTO commands. Put differently, imagine an enemy has destroyed the PLOT and DRAWTO commands but you have to plot pixels anyway. Thus, determine the video memory base and manipulate the video memory directly. While the result is going to be very slow, one can practice the low-level bit-by-bit reasoning and manipulation.
10 GRAPHICS 8 20 FOR I=0 TO 49 30 X=I:Y=I:GOSUB 200 40 NEXT I 50 FOR I=0 TO 49 60 X=I+1:Y=I:GOSUB 200 70 NEXT I 80 FOR I=0 TO 49:REM REPEAT FOR TEST 90 X=I:Y=I:GOSUB 200 100 NEXT I 110 END 200 REM PLOT X,Y FOR MODE 8 WITH COLOR 1 205 REM -------------------------------- 210 P=PEEK(560)+PEEK(561)*256 220 VB=PEEK(P+4)+PEEK(P+5)*256: REM VIDEOBASE 230 BYTEOFFSET=INT(X/8)+320/8*Y 240 MOD=X-INT(X/8)*8 250 BITPAT=2^(7-MOD) 260 A=PEEK(VB+BYTEOFFSET):B=BITPAT 270 GOSUB 400 360 POKE VB+BYTEOFFSET, ORED 370 RETURN 400 REM BYTE-OR A, B 405 REM ------------------ 410 ORED=0 420 FOR J=0 TO 7 430 ORED=2*ORED 440 IF A>=128 OR B>=128 THEN ORED=ORED+1 450 IF A>=128 THEN A=A-128 460 IF B>=128 THEN B=B-128 470 A=A*2: B=B*2 480 NEXT J 490 RETURN
Above, we use a custom routine for byte-OR to compensate for its lack in Atari BASIC; an alternative would be to use an assembly routine embedded in BASIC.
Exercise: Modify the above code to make it mode efficient. Ideas: 1) determining the videobase each time we want to draw a pixel is an avoidable overhead; 2) having a separate OR routine to jump to creates an overhead, given jumps are slow in Atari BASIC (inlining helps a little); 3) having a bit-setting routine rather than a general OR routine could turn to be faster.
Exercise: Create a routine that takes color 0 or 1 as an input, and plots the pixel in that color for mode 8. Thus, the routine should be able not only to set a pixel but also to erase it. Demonstrate the routine by drawing lines in the colors.
A solution:
10 GRAPHICS 8 20 DL=PEEK(560)+PEEK(561)*256 30 VB=PEEK(DL+4)+PEEK(DL+5)*256:REM VIDEOBASE 40 PLC=1 50 FOR I=0 TO 49:X=I:Y=I:GOSUB 200:NEXT I 60 FOR I=0 TO 49:X=I+1:Y=I:GOSUB 200:NEXT I 70 FOR I=0 TO 49:X=I:Y=I:GOSUB 200:NEXT I 80 PLC=0 90 FOR I=0 TO 49:X=I:Y=I:GOSUB 200:NEXT I 100 END 200 REM PLOT X,Y FOR MODE 8 WITH COLOR PLC 210 REM ---------------------------------- 220 XE=INT(X/8) 230 BYTEOFFSET=XE+40*Y 240 BITOFF=X-XE*8 260 A=PEEK(VB+BYTEOFFSET) 270 RES=0 280 FOR J=0 TO 7 290 RES=2*RES 300 IF PLC=1 AND (A>=128 OR J=BITOFF) THEN RES=RES+1 310 IF PLC=0 AND (A>=128 AND J<>BITOFF) THEN RES=RES+1 320 IF A>=128 THEN A=A-128 330 A=A*2 340 NEXT J 350 POKE VB+BYTEOFFSET, RES 360 RETURN
Bitwise OR
editByte-sized bitwise OR:
10 REM BYTE-OR A, B 20 INPUT A,B:GOSUB 400:PRINT RES:END 400 RES=0 410 FOR J=0 TO 7 420 RES=2*RES 430 IF A>=128 OR B>=128 THEN RES=RES+1 440 IF A>=128 THEN A=A-128 450 IF B>=128 THEN B=B-128 460 A=A*2: B=B*2 470 NEXT J 480 RETURN
Modifying the above code to obtain AND or XOR or to obtain larger value size (e.g. 16-bit) is a very easy exercise. The Stack Overflow link below contains C code for bitwise OR, AND and XOR.
Links:
- discrete mathematics - Is it possible to implement bitwise operators using integer arithmetic?, stackoverflow.com
Plotting joystick-controlled vehicle
editThe following plots a joystick-controlled vehicle. The vehicle is a single pixel that has a direction (not just orthogonal) and leaves behind a trace.
10 GRAPHICS 8 15 TWOPI=2*3.141592 20 ANGLERAD=0 30 XRE=160:YRE=80 35 COLOR 1 40 PLOT INT(XRE), INT(YRE) 50 XRE=XRE+COS(ANGLERAD) 60 YRE=YRE+SIN(ANGLERAD) 70 REM ANGLERAD=ANGLERAD+RND(1)*0.1-0.5*0.1 80 IF STICK(0)<>11 THEN GOTO 100:REM 11=RIGHT 90 ANGLERAD=ANGLERAD-0.1 100 IF STICK(0)<>7 THEN GOTO 120:REM 7=LEFT 110 ANGLERAD=ANGLERAD+0.1 120 IF ANGLERAD<TWOPI THEN GOTO 140 130 ANGLERAD=ANGLERAD-TWOPI 140 IF ANGLERAD>=0 THEN GOTO 160 150 ANGLERAD=ANGLERAD+TWOPI 160 GOTO 40
Plotting a spiral
editPlotting a spiral with a linearly growing radius:
10 GRAPHICS 8 20 COLOR 1 30 R=0 40 PLOT 160,80 50 FOR X=0 TO 8*2*3.141592 STEP 0.1 60 DRAWTO 160+INT(R*COS(X)),80+INT(R*SIN(X)) 70 R=R+0.1 80 NEXT X
Plotting a spiral with an exponentially growing radius:
10 GRAPHICS 8 20 COLOR 1 30 R=2 40 PLOT 160,80 50 FOR X=0 TO 6*2*3.141592 STEP 0.1 60 DRAWTO 160+INT(R*COS(X)),80+INT(R*SIN(X)) 70 R=R*1.01 80 NEXT X
Monte Carlo area calculation
editWe can calculate unit circle area using Monte Carlo integration, which will give us the value of pi. We will consider a circle with the radius of 128. We will visualize the random points that landed in the circle. There are more efficient methods of pi calculation; this is to illustrate Monte Carlo area calculation, which can be adapted for a broad range of shapes.
10 GRAPHICS 8: COLOR 1 20 I128SQ=128*128 30 FOR I=1 TO 10000 40 X=PEEK(53770)-128 50 Y=PEEK(53770)-128 60 RSQ=X*X+Y*Y 70 IF RSQ <= I128SQ THEN INSIDECNT=INSIDECNT+1 80 IF RSQ <= I128SQ THEN PLOT INT((X+128)/2), INT((Y+128)/2) 90 TOTALCNT=TOTALCNT+1 100 NEXT I 110 AREARATIO=INSIDECNT/TOTALCNT 120 PIAPPROX=AREARATIO*4 130 PRINT "PI ESTIMATE: ";PIAPPROX 140 REM CIRCLE AREA=PI*R^2; SQUARE AREA=4*R^2
Calculating median
editTo calculate the median, we need to sort the sequence.
10 DIM V(101) 20 FOR I=1 TO 101 30 V(I) = PEEK(53770):REM RANDOM in 0-255 40 NEXT I 50 FOR I=1 TO 101: ? V(I);", ";:NEXT I:PRINT 60 GOSUB 200:REM BUBBLE SORT 70 PRINT "SORTED: " 80 FOR I=1 TO 101: ? V(I);", ";:NEXT I:PRINT 90 PRINT "MEDIAN: ";V(51) 100 END 200 REM BUBBLE SORT ON V 210 SW=0:REM SWAPPED 220 FOR I=1 TO 100 230 IF V(I+1)<V(I) THEN S=V(I):V(I)=V(I+1):V(I+1)=S:SW=1 240 NEXT I 250 IF SW=1 THEN GOTO 210 260 RETURN
Plotting trigonometric functions
editPlotting trigonometric functions (sine, cosine, arcus tangent):
10 GRAPHICS 8 20 FOR I=0 TO 319 30 X=I/320*8*2 40 PLOT I,80-40*SIN(X) 50 PLOT I,80-40*COS(X) 55 PLOT I,80-40*ATN(X) 60 NEXT I
Calculating prime numbers
edit10 FOR I=2 TO 100 20 PR=1 30 FOR D=2 TO INT(SQR(I)) 40 IF D<>I AND I/D=INT(I/D) THEN PR=0 50 NEXT D 60 IF PR=1 THEN PRINT I 70 NEXT I
Calculating pi
editOne can get an acceptable approximation of pi via PI=4*ATN(1) or PI=2*ATN(1E97), the latter turning out to be more accurate. An algorithm for calculation of many digits coded in Atari BASIC is in the link.
Further reading:
- Pi, rosettacode.org
Plotting a rectangular spiral
edit10 GRAPHICS 8: COLOR 1 20 FOR I=0 TO 39 30 PLOT 0+2*I,0+2*I 40 IF I>0 THEN PLOT 0+2*I-2,0+2*I 50 DRAWTO 319-2*I,0+2*I 60 DRAWTO 319-2*I,159-2*I 70 DRAWTO 0+2*I,159-2*I 80 DRAWTO 0+2*I,0+2*I+2 90 NEXT I
A LCG pseudorandom number generator
editThere is the built-in RND pseudorandom generator function, which probably uses the 8-bit pseudorandom generator provided by the POKEY chip's 17-bit linear feedback shift register available at address 53770. This generator does not provide seeding and reproducibility; sources indicate that POKEY updates the value of its generator each CPU cycle, regardless of whether the value is being read. If one wants to have seeding and reproducibility, one can use e.g. linear congruential generator, in which the next integral value is calculated from the previous one using the formula x := a*x + c mod m for well chosen values of a, c and m. Since Atari BASIC numbers are floating point numbers with guaranteed 9 decimal places (rather than integers), one has to use m much smaller than 2^32 or 2^31. In the following, from Wikipedia's parameter combinations we select the one indicated for ZX81 (it traces to no source!), for which both the m and a*m fit with full integer precision into the Atari BASIC float. We convert the generated numbers to 8-bit integer values and plot the counts of value visits for a crude visual verification of the uniformity of the distribution. An alternative with a much longer period would be Wichmann–Hill, which combines three LCGs with the m of ca. 30000 and the a of ca. 170, well fitting into Atari BASIC float.
10 REPEATCOUNT=1000000 20 DIM VALCOUNT(256) 30 GRAPHICS 8: COLOR 1 35 RX=0:REM OR A DIFFERENT SEED 40 FOR I=1 TO 256: VALCOUNT(I)=0: NEXT I 50 FOR I=1 TO REPEATCOUNT 55 GOSUB 200:REM RX:=RND 60 R=RX-256*INT(RX/256):REM MOD 70 VALCOUNT(R+1)=VALCOUNT(R+1)+1 80 IF VALCOUNT(R+1)>160 THEN END:REM SCREEN LIMIT 90 PLOT R, VALCOUNT(R+1)-1 100 NEXT I 110 END 200 REM NEXT RND 210 RX=75*RX+74 220 RX=RX-65537*INT(RX/65537):REM MOD 260 RETURN
Further reading:
- Linear congruential generator, wikipedia.org
- Wichmann–Hill, wikipedia.org
Hangman
editThe game of hangman is beautifully simple to implement, making it a good fit as an exerise for novice kid programmers. In the following, we omit any graphics and only count lives lost/left. Note that the words in the DATA statement are entered, perhaps surprisingly, without quotation marks.
10 WORDMAXLEN=10:LIVES=5:WRDCOUNT=10 20 DIM WORD$(WORDMAXLEN), GUESS$(1) 30 DATA GROUND,SNEEZE,MOTHER,SISTER,PILLAR,SMILED,POOLED,TOTALS,FROZEN,RABBIT 40 IDX=INT(RND(1)*WRDCOUNT) 50 I=0 60 READ WORD$ 70 IF I<IDX THEN I=I+1: GOTO 60 80 DIM LTRUNKN(WORDMAXLEN) 90 FOR I=1 TO LEN(WORD$):LTRUNKN(I)=1:NEXT I 100 LTGC=LEN(WORD$):REM LETTER TO GUESS COUNT 110 FOR I=1 TO LEN(WORD$) 120 IF LTRUNKN(I)=1 THEN PRINT "*"; 130 IF LTRUNKN(I)=0 THEN PRINT WORD$(I,I); 140 NEXT I 150 PRINT 160 IF LTGC=0 THEN PRINT "COMPLETED!":END 170 INPUT GUESS$ 180 LTGCO=LTGC 190 FOR I=1 TO LEN(WORD$) 200 IF WORD$(I,I) = GUESS$ THEN LTRUNKN(I) = 0:LTGC=LTGC-1 210 NEXT I 220 IF LTGCO=LTGC THEN LIVES=LIVES-1: PRINT "LIVES LEFT: ";LIVES 230 IF LIVES = 0 THEN PRINT "NO MORE LIVES.":END 240 GOTO 110
Binary expansion
editWe can calculate the binary expansion of a number between 0 and 1, an analogue of decimal expansion. The bits after decimal points represent 1/2, 1/4, 1/8, etc. The task seems simple enough for kids to figure it out. Tests values: 0.5: 0.1; 0.25: 0.01; 0.125: 0.001; 0.625: 101; 0.7: 0.10110011001100110011....
There is an ambiguity: e.g. 0.5 can be rendered as 0.1 or as 0.01111... The requirement is to produce the former, arguably more natural, output, in part since we cannot really output an infinite sequence, so the output sequence would be slightly imprecise in the latter option but not in the former option.
10 PRINT "BINARY EXPANSION" 20 INPUT X 30 IF X<0 OR X>=1 THEN PRINT "BAD X":END 40 BV=0.5 50 PRINT "0."; 60 FOR I=1 TO 30 70 IF X >= BV THEN GOTO 90 80 PRINT "0";:GOTO 100 90 X=X-BV:PRINT "1"; 100 BV=BV/2 110 IF X=0 THEN I=30 120 NEXT I 130 PRINT 140 GOTO 20
An alternative implementation by repeated multiplying the X by 2.
10 PRINT "BINARY EXPANSION" 20 INPUT X 30 IF X<0 OR X>=1 THEN PRINT "BAD X":END 35 PRINT "0."; 40 FOR I=1 TO 30 50 X=X*2 60 IF X<1 THEN PRINT "0"; 70 IF X>=1 THEN PRINT "1";:X=X-1 80 IF X=0 THEN I=30 90 NEXT I 100 PRINT 110 GOTO 20
Going in the other direction, from binary to decimal, is also very simple and is left as an exercise.
Ternary expansion
editUsing a method similar to binary expasion, we can calculate the ternary expansion. We can also add a verification part, which converts the ternary expansion back to the decimal form.
5 DIM S$(37) 10 PRINT "TERNARY EXPANSION" 20 INPUT X 30 IF X<0 OR X>=1 THEN PRINT "BAD X":END 40 S$="0." 50 FOR I=1 TO 35 60 X=X*3 70 IF X<1 THEN S$(I+2,I+2)="0" 80 IF X>=2 THEN S$(I+2,I+2)="2":X=X-2 90 IF X>=1 THEN S$(I+2,I+2)="1":X=X-1 100 IF X=0 THEN I=35 110 NEXT I 120 PRINT S$ 130 REM ----Verification---- 140 W=1/3:X=0 150 FOR I=1 TO 35 160 IF S$(I+2,I+2)="1" THEN X=X+W 165 IF S$(I+2,I+2)="2" THEN X=X+2*W 170 W=W/3 180 NEXT I 190 PRINT "RECONSTRUCTED X: ";X
Base n expansion
editUsing the exercise of ternary expansion above as a warm up, we can implement general base n expansion. For base greater than 16, we output the digits in decimal separated by semicolon. For base greater than 10 and smaller than or equal to 16, we output A-F as digits in addition to 0-9. We include verification by calculating the inverse, where the inverse is simpler.
10 DIM S(30), SEP$(1) 20 PRINT "BASE N EXPANSION" 30 PRINT "X:";:INPUT X:OX=X 40 IF X<0 OR X>=1 THEN PRINT "BAD X":END 50 PRINT "BASE:";:INPUT B 60 B=INT(B) 70 IF B<2 THEN PRINT "BAD BASE":END 80 L=30:LF=0:REM LENGTH and LENGTH FOUND 90 FOR I=1 TO 30 100 X=X*B 110 IF X<1 THEN S(I)=0 120 FOR J=B-1 TO 1 STEP -1 130 IF X>=J THEN S(I)=J:X=X-J 140 NEXT J 150 IF LF=0 AND X=0 THEN LF=1:L=I 160 NEXT I 170 PRINT "0."; 180 IF B>10 AND B<=16 THEN GOTO 230 190 SEP$="":IF B>10 THEN SEP$=";" 200 FOR I=1 TO L:PRINT S(I);SEP$;:NEXT I 210 PRINT 220 GOTO 300 230 REM BASE 11 TO 16 240 FOR I=1 TO L 250 IF S(I)<10 THEN PRINT S(I); 260 IF S(I)>=10 THEN PRINT CHR$(65+S(I)-10); 270 NEXT I 280 PRINT 290 REM -------- Verification -------- 300 RX=0 310 W=1/B 320 FOR I=1 TO 30 330 RX=RX+S(I)*W 340 W=W/B 350 NEXT I 360 PRINT "RECON.X & DELTA: ";RX;"; ";ABS(OX-RX)
Number system conversion
editWe can implement number system conversion for positive integers: the user enters a decimal number and the target base; the number converted to the targer base is output. The idea used is rather similar to the base n expansion above. The reader can write a more general program as an exercise: the input is not in decimal and is a string, and there are both input base and output base specified.
10 DIM S(100) 20 PRINT "NUMBER SYSTEM CONVERSION" 30 PRINT "X: ";:INPUT X 40 PRINT "BASE: ";:INPUT B 50 TH=1:REM THRESHOLD 60 TH=TH*B 70 IF TH<=X THEN GOTO 60 80 I=1 90 TH=TH/B 100 IF X<TH THEN S(I)=0 110 FOR J=B-1 TO 1 STEP -1 120 IF X>=TH*J THEN S(I)=J:X=X-TH*J:J=1 130 NEXT J 140 IF TH>1 THEN I=I+1:GOTO 90 150 IF B>10 AND B<=16 THEN GOTO 190 160 DIM SEP$(1):SEP$="":IF B>10 THEN SEP$=";" 170 FOR K=1 TO I: PRINT S(K);SEP$;:NEXT K:PRINT 180 GOTO 250 190 REM BASE 11-16 200 FOR K=1 TO I 210 IF S(K)<10 THEN PRINT S(K); 220 IF S(K)>=10 THEN PRINT CHR$(65+S(K)-10); 230 NEXT K 240 PRINT 250 REM ---- Verification ---- 260 X=0:W=1 270 FOR K=I TO 1 STEP -1:X=X+S(K)*W:W=W*B:NEXT K 280 PRINT "RECON.X: ";X
Gollatz conjecture
editAssignment: explore Gollazt conjecture for as many initial integers as possible. Find the first integer for which the exploration of the conjecture exceeds the integer precision limit 1E9. Optionally, use caching in memory to speed it up.
One can show Gollatz conjecture even for some integers that are higher than the first integer for which the precision limit is hit.
Exploring the Gollatz conjecture on a small 8-bit computer is very inefficient but can be done as an exercise. One can try to figure out small optimizations to achieve faster exploration.
A simple naive algorithm with no caching:
10 I=1 20 X=I:PRINT X;";"; 30 IF INT(X/2)=X/2 THEN X=X/2: GOTO 50 40 X=X*3+1 50 PRINT X;";"; 55 IF X>1E9 THEN PRINT "LIMIT EXCEEDED: END 60 IF X<>1 AND X>I THEN GOTO 30 70 PRINT:PRINT 80 I=I+2:GOTO 20
A variant with a byte array (a string) used to refuse to explore already visited/seen numbers:
10 CLEN=30000:DIM C$(CLEN) 20 C$="0":C$(CLEN-1)="0":C$(2)=C$:REM FAST INIT 40 INPUT I:REM DEFAULT 1 AND SHOULD BE ODD 45 IF INT(I/2)=I/2 THEN PRINT "MUST BE ODD":END 50 X=I:PRINT X;";"; 55 IF I<=CLEN THEN IF C$(I,I)="1" THEN PRINT "SEEN":PRINT:I=I+2:GOTO 50 60 IF INT(X/2)=X/2 THEN X=X/2: GOTO 80 70 X=X*3+1 80 PRINT X;";"; 85 IF X>1E9 THEN PRINT "LIMIT EXCEEDED": END 95 IF X<=CLEN THEN C$(X,X)="1" 100 IF X<>1 AND X>I THEN GOTO 60 110 PRINT:PRINT 120 I=I+2:GOTO 50
A variant with a byte array (a string) used to refuse to explore already visited/seen numbers and another byte array to prevent considering a cycle in the sequence as a descent to 1:
10 CLEN=18000:DIM C$(CLEN):DIM D$(CLEN) 20 C$="0":C$(CLEN-1)="0":C$(2)=C$:REM FAST INIT 40 INPUT I:REM DEFAULT 1 AND SHOULD BE ODD 45 IF INT(I/2)=I/2 THEN PRINT "MUST BE ODD":END 50 X=I:PRINT X;";"; 55 IF I<=CLEN THEN IF C$(I,I)="1" THEN PRINT "SEEN":PRINT:I=I+2:GOTO 50 56 D$="0":D$(CLEN-1)="0":D$(2)=C$:REM FAST INIT 60 IF INT(X/2)=X/2 THEN X=X/2: GOTO 80 70 X=X*3+1 80 PRINT X;";"; 85 IF X>1E9 THEN PRINT "LIMIT EXCEEDED": END 90 IF X>I AND X<=CLEN THEN IF C$(X,X)="1" AND D$(X,X)="0" THEN PRINT "SEEN";:GOTO 110 95 IF X<=CLEN THEN C$(X,X)="1": D$(X,X)="1" 100 IF X<>1 AND X>I THEN GOTO 60 110 PRINT:PRINT 120 I=I+2:GOTO 50
Above, we exit as soon as we visit a number higher than 1E9, the highest integer that is part of contiguous sequence of integers.
Given the very limited memory, one would want to store the flags on bit level rather than byte level. However, Atari BASIC lacks facilities to support this, especially bit test, which can be done with BITAND. One could implement it anyway; it is left as an exercise for the reader.
The caching optimization above is just an exercise with no impact once we start to explore the higher echelons. This is so even if we progressively let the cache start not at 1 but rather at, say, 1000, 2000, etc. Since, the operations of 3x+1 and x/2 work on scale, and the higher the starting number, the more removed are the next items from the baseline, and the less likely the unit-contiguous cache is going to be of any use.
Some questions: does it pay off to write a bit-array code given the lack of bitwise facilities? Does it pay off to have two arrays C$ and D$ with half of the memory that could be allocated to array C$ only? Answers to these questions depend on how soon the limit 1E9 is hit.
A variant assignment: output initial values of the Gollatz process whose orbit/trajectory reached previously unreached heights. This is https://oeis.org/A006884. Code without caching:
5 GOSUB 100:ITIME=TIME 10 I=1 15 MAX=1:OMAX=0 20 X=I:OMAX=MAX 30 IF INT(X/2)=X/2 THEN X=X/2: GOTO 50 40 X=X*3+1 50 IF X>MAX THEN MAX=X 60 IF X>1E9 THEN PRINT "LIMIT EXCEEDED":END 70 IF X<>1 AND X>I THEN GOTO 30 80 IF MAX>OMAX THEN GOSUB 100:PRINT I;" (";TIME-ITIME;" s), "; 90 I=I+2:GOTO 20 100 TIME=INT((PEEK(18) * 65536 + PEEK(19) * 256 + PEEK(20))/50):RETURN
The same using a single-array caching:
5 GOSUB 130:ITIME=TIME 10 CLEN=30000:DIM C$(CLEN) 20 C$="0":C$(CLEN-1)="0":C$(2)=C$:REM FAST INIT 30 MAX=1:OMAX=0 40 I=1 50 X=I:OMAX=MAX 55 IF I<=CLEN THEN IF C$(I,I)="1" THEN I=I+2:GOTO 50 60 IF INT(X/2)=X/2 THEN X=X/2: GOTO 80 70 X=X*3+1 80 IF X>MAX THEN MAX=X 85 IF X>1E9 THEN PRINT "LIMIT EXCEEDED": END 95 IF X<=CLEN THEN C$(X,X)="1" 100 IF X<>1 AND X>I THEN GOTO 60 110 IF MAX>OMAX THEN GOSUB 130:PRINT I;" (";TIME-ITIME;" s), "; 120 I=I+2:GOTO 50 130 TIME=INT((PEEK(18) * 65536 + PEEK(19) * 256 + PEEK(20))/50):RETURN
The same using a two-array caching:
5 GOSUB 130:ITIME=TIME 10 CLEN=18000:DIM C$(CLEN):DIM D$(CLEN) 20 C$="0":C$(CLEN-1)="0":C$(2)=C$:REM FAST INIT 30 MAX=1:OMAX=0 40 I=1 50 X=I:OMAX=MAX 55 IF I<=CLEN THEN IF C$(I,I)="1" THEN I=I+2:GOTO 50 56 D$="0":D$(CLEN-1)="0":D$(2)=C$:REM FAST INIT 60 IF INT(X/2)=X/2 THEN X=X/2: GOTO 80 70 X=X*3+1 80 IF X>MAX THEN MAX=X 85 IF X>1E9 THEN PRINT "LIMIT EXCEEDED": END 90 IF X>I AND X<=CLEN THEN IF C$(X,X)="1" AND D$(X,X)="0" THEN GOTO 110 95 IF X<=CLEN THEN C$(X,X)="1": D$(X,X)="1" 100 IF X<>1 AND X>I THEN GOTO 60 110 IF MAX>OMAX THEN GOSUB 130:PRINT I;" (";TIME-ITIME;" s), "; 120 I=I+2:GOTO 50 130 TIME=INT((PEEK(18) * 65536 + PEEK(19) * 256 + PEEK(20))/50):RETURN
The single-array variant of the max-height start value finder seems to be generally fastest.
Sieve of Eratosthenes
editAssignment 1: implement the method of searching for prime numbers known as the sieve of Erathosthenes and report all the primes found. Aim for maximum prime number discoverable in this way. Use byte array (a string).
Assignment 2: as above but use bit array rather than byte array. Hint: use integer arithmetic to emulate bitwise access; warning: this is going to be very slow.
A solution to assignment 1:
10 PRINT "SIEVE OF ERATOSTHENES" 20 ILEN=32767 30 DIM I$(ILEN):REM IS COMPOSITE 40 I$="0":I$(ILEN-1)="0":I$(2)=I$:REM FAST INIT 50 N=2 60 PRINT N;","; 70 IF N*2>ILEN THEN GOTO 90 80 FOR J=N*2 TO ILEN STEP N:I$(J,J)="1":NEXT J 90 N=N+1:IF N>ILEN THEN END 100 IF I$(N,N)="1" THEN GOTO 90 110 GOTO 60:REM NEXT PRIME N
The above requires quite a bit of patience but is tolerable. Recall that the finding of prime numbers becomes increasingly faster as the prime becomes bigger, a property of the sieve.
A solution to assignment 2:
10 PRINT "SIEVE OF ERATOSTHENES" 20 ILEN=50:IBITLEN=ILEN*8 30 REM ILEN CAN BE 32767 BUT IT GETS ASTRONOMICALLY SLOW 40 DIM I$(ILEN):REM IS COMPOSITE 50 IADDR=ADR(I$) 60 I$=CHR$(0):I$(ILEN-1)=CHR$(0):I$(2)=I$:REM FAST INIT 70 N=2 80 PRINT N;", "; 90 IF N*2>IBITLEN THEN GOTO 130 100 FOR J=N*2 TO IBITLEN STEP N 110 X=J-1:GOSUB 500 120 NEXT J 130 N=N+1:IF N>IBITLEN THEN GOTO 160 140 X=N-1:GOSUB 400:IF BSET=1 THEN GOTO 130 150 GOTO 80:REM NEXT PRIME N 160 END 400 REM ---- I(X) BIT ARRAY READ ACCESS 405 REM INPUT X ZERO-INDEXED, OUTPUT BSET OF 0 OR 1 410 BYTEOFF=INT(X/8) 420 BITOFF=X-INT(X/8)*8 430 VL=PEEK(IADDR+BYTEOFF) 440 BVAL=128:BCNT=0 450 BSET=0:IF VL>=BVAL THEN BSET=1:VL=VL-BVAL 460 IF BCNT<>BITOFF THEN BCNT=BCNT+1:BVAL=BVAL/2:GOTO 450 470 RETURN 500 REM ---- I(X) BIT ARRAY WRITE ACCESS SETTING TO 1 510 REM SETTING TO ZERO NOT IMPLEMENTED 520 REM INPUT X ZERO-INDEXED, OUTPUT VOID 530 GOSUB 400:IF BSET=1 THEN RETURN 540 BYTEOFF=INT(X/8) 550 BITOFF=X-INT(X/8)*8 560 BVAL=128 570 IF BITOFF>0 THEN BVAL=BVAL/2:BITOFF=BITOFF-1:GOTO 570 580 POKE IADDR+BYTEOFF,PEEK(IADDR+BYTEOFF)+BVAL 590 RETURN
The above bit manipulation via integer arithmetic is so slow that the above exercise is pointless: one would not dream of setting ILEN to 32767; setting ILEN to 50 is slow enough.
Reader challenge: modify the above by implementing the bit array access in assembly embedded in BASIC in a DATA statement.
Minesweeper
editImplementing Minesweeper in the character mode 0 is a reasonably simple exercise, although it requires some low-level tricks. There is going to be a cursor controlled by joystick and the cursor is to be indicated using inverse character (base code + 128). Optionally, pressing M key shall toggle a mine mark on the field. Pressing fire shall uncover the field unless the field has a mine mark. Here it goes.
10 DIM M(40+2,24+2):REM MINES 20 XSZ=20:YSZ=12:MRATIO=0.2:REM MAX 40x24x0.25 30 BKCHR=ASC("-")-32:REM BACKGROUND CHAR CODE 40 MCHR=96:REM MINE CHAR CODE 50 CLSCHR=125 60 P=PEEK(560)+PEEK(561)*256 70 VB=PEEK(P+4)+PEEK(P+5)*256:REM VIDEOBASE 80 PRINT CHR$(CLSCHR):PRINT "GENERATING MINES..." 90 FOR X=1 TO XSZ+2:FOR Y=1 TO YSZ+2: M(X,Y)=0: NEXT Y: NEXT X 100 MCNT=INT(XSZ*YSZ*MRATIO) 110 FOR I=1 TO MCNT 120 X=INT(RND(0)*XSZ)+1 130 Y=INT(RND(0)*YSZ)+1 140 IF M(X+1,Y+1)=1 THEN GOTO 120 150 M(X+1,Y+1)=1 160 NEXT I 170 SPCLEFT=XSZ*YSZ-MCNT 175 PRINT CHR$(CLSCHR) 180 FOR Y=1 TO YSZ 185 FOR X=1 TO XSZ:POKE VB+(Y-1)*40+X-1, BKCHR:NEXT X 190 NEXT Y 200 CX=1:CY=1:REM CURSOR X AND Y 210 VD=128:GOSUB 600 220 REM ---- HANDLE JOYSTICK EVENTS AND KEY PRESS 230 IF STICK(0)<>13 THEN GOTO 270:REM DOWN 240 GOSUB 600:REM TOGGLE INVERSE 250 CY=CY+1:IF CY>YSZ THEN CY=YSZ 260 GOSUB 600:REM TOGGLE INVERSE 270 IF STICK(0)<>14 THEN GOTO 310:REM UP 280 GOSUB 600:REM TOGGLE INVERSE 290 CY=CY-1:IF CY<1 THEN CY=1 300 GOSUB 600:REM TOGGLE INVERSE 310 IF STICK(0)<>7 THEN GOTO 350:REM RIGHT 320 GOSUB 600:REM TOGGLE INVERSE 330 CX=CX+1:IF CX>XSZ THEN CX=XSZ 340 GOSUB 600:REM TOGGLE INVERSE 350 IF STICK(0)<>11 THEN GOTO 390:REM LEFT 360 GOSUB 600:REM TOGGLE INVERSE 370 CX=CX-1:IF CX<1 THEN CX=1 380 GOSUB 600:REM TOGGLE INVERSE 390 IF STRIG(0)<>0 THEN GOTO 470:REM FIRE BUTTON 400 IF PEEK(VB+(CY-1)*40+CX-1)<>128+BKCHR THEN GOTO 470 410 IF M(CX+1,CY+1)=1 THEN POSITION 0,0:PRINT "BOOM!";:GOTO 500 420 SPCLEFT=SPCLEFT-1:IF SPCLEFT=0 THEN POSITION 0,0:PRINT "WIN!";:GOTO 500 430 REM MINE COUNT ON SQUARE 440 MSQ=0:FOR X=0 TO 2:FOR Y=0 TO 2:MSQ=MSQ+M(CX+X,CY+Y):NEXT Y:NEXT X 450 IF MSQ=0 THEN POKE VB+(CY-1)*40+CX-1, ASC(" ")-32+128 460 IF MSQ<>0 THEN POKE VB+(CY-1)*40+CX-1, ASC("0")-32+MSQ+128 470 IF PEEK(764)=37 THEN POKE 764, 255: GOTO 490: REM KEY M 480 GOTO 495 490 NV=MCHR+128:IF PEEK(VB+(CY-1)*40+CX-1)=MCHR+128 THEN NV=BKCHR+128 493 POKE VB+(CY-1)*40+CX-1, NV 495 FOR I=1 TO 5:NEXT I:GOTO 220 500 FOR I=1 TO 30:NEXT I:IF STRIG(0)<>0 THEN GOTO 500 510 PRINT CHR$(CLSCHR) 520 PRINT "NEW GAME (1 OR 0)";:INPUT A:IF A=0 THEN END 530 PRINT "USE THE SAME BOARD (1 OR 0)";:INPUT A 540 IF A=1 THEN GOTO 170 550 GOTO 80 600 REM ---- TOGGLE INVERSE FOR A CHAR ON SCREEN; USE VB, CY, CX 610 CHAR=PEEK(VB+(CY-1)*40+CX-1) 620 NCHAR=CHAR+128:IF CHAR>=128 THEN NCHAR=CHAR-128 630 POKE VB+(CY-1)*40+CX-1,NCHAR:RETURN
Above, we use low-level tricks: 1) we access the video memory of the character mode directly via POKE and PEEK rather than using PRINT. The character values in the video memory are not the same as ATASCII and need a mapping; 2) we test for key press via POKE and PEEK.
Even for the size of 20x12 (the maximum is 40x24), it takes several seconds to generate the mines and several more seconds to draw the initial screen with its not yet explored fields. This suggests that Atari BASIC is too slow for most kinds of even simple games. Nonetheless, one could create a simple maze game or Sokoban, accepting the time it initially takes to draw the screen. Comparing the performance of Atari BASIC for Minesweeper with e.g. Boulder Dash shows how hugely faster assembly was on the Atari; Boulder Dash screens are large (in part not shown) and are drawn instantly.
An assignment: expand the above code to implement automatic uncovering of the fields around zero-noted fields.
A solution:
10 DIM M(40+2,24+2),UFSTACKX(40*24), UFSTACKY(40*24):REM M: MINES 20 XSZ=20:YSZ=12:MRATIO=0.2:REM MAX 40x24x0.25 30 BKCHR=ASC("-")-32:REM BACKGROUND CHAR CODE 40 MCHR=96:REM MINE CHAR CODE 50 CLSCHR=125 60 P=PEEK(560)+PEEK(561)*256 70 VB=PEEK(P+4)+PEEK(P+5)*256:REM VIDEOBASE 80 PRINT CHR$(CLSCHR):PRINT "GENERATING MINES..." 90 FOR X=1 TO XSZ+2:FOR Y=1 TO YSZ+2: M(X,Y)=0: NEXT Y: NEXT X 100 MCNT=INT(XSZ*YSZ*MRATIO) 110 FOR I=1 TO MCNT 120 X=INT(RND(0)*XSZ)+1 130 Y=INT(RND(0)*YSZ)+1 140 IF M(X+1,Y+1)=1 THEN GOTO 120 150 M(X+1,Y+1)=1 160 NEXT I 170 SPCLEFT=XSZ*YSZ-MCNT 171 UFSTACKP=1 175 PRINT CHR$(CLSCHR) 180 FOR Y=1 TO YSZ 185 FOR X=1 TO XSZ:POKE VB+(Y-1)*40+X-1, BKCHR:NEXT X 190 NEXT Y 200 CX=1:CY=1:REM CURSOR X AND Y 210 GOSUB 600 220 REM ---- HANDLE JOYSTICK EVENTS AND KEY PRESS 230 IF STICK(0)<>13 THEN GOTO 270:REM DOWN 240 GOSUB 600:REM TOGGLE INVERSE 250 CY=CY+1:IF CY>YSZ THEN CY=YSZ 260 GOSUB 600:REM TOGGLE INVERSE 270 IF STICK(0)<>14 THEN GOTO 310:REM UP 280 GOSUB 600:REM TOGGLE INVERSE 290 CY=CY-1:IF CY<1 THEN CY=1 300 GOSUB 600:REM TOGGLE INVERSE 310 IF STICK(0)<>7 THEN GOTO 350:REM RIGHT 320 GOSUB 600:REM TOGGLE INVERSE 330 CX=CX+1:IF CX>XSZ THEN CX=XSZ 340 GOSUB 600:REM TOGGLE INVERSE 350 IF STICK(0)<>11 THEN GOTO 390:REM LEFT 360 GOSUB 600:REM TOGGLE INVERSE 370 CX=CX-1:IF CX<1 THEN CX=1 380 GOSUB 600:REM TOGGLE INVERSE 390 IF STRIG(0)<>0 THEN GOTO 440:REM FIRE BUTTON 400 IF PEEK(VB+(CY-1)*40+CX-1)<>128+BKCHR THEN GOTO 440 410 IF M(CX+1,CY+1)=1 THEN POSITION 0,0:PRINT "BOOM!";:GOTO 500 420 FX=CX:FY=CY:ADDINV=128:GOSUB 700:REM UNCOVER FIELD 430 IF SPCLEFT=0 THEN POSITION 0,0:PRINT "WIN!";:GOTO 500 440 IF PEEK(764)=37 THEN POKE 764, 255: GOTO 460: REM KEY M 450 GOTO 480 460 NV=MCHR+128:IF PEEK(VB+(CY-1)*40+CX-1)=MCHR+128 THEN NV=BKCHR+128 470 POKE VB+(CY-1)*40+CX-1, NV 480 FOR I=1 TO 5:NEXT I:GOTO 220 500 FOR I=1 TO 30:NEXT I:IF STRIG(0)<>0 THEN GOTO 500 510 PRINT CHR$(CLSCHR) 520 PRINT "NEW GAME (1 OR 0)";:INPUT A:IF A=0 THEN END 530 PRINT "USE THE SAME BOARD (1 OR 0)";:INPUT A 540 IF A=1 THEN GOTO 170 550 GOTO 80 600 REM ---- TOGGLE INVERSE FOR A CHAR ON SCREEN; USE VB, CY, CX 610 CHAR=PEEK(VB+(CY-1)*40+CX-1) 620 NCHAR=CHAR+128:IF CHAR>=128 THEN NCHAR=CHAR-128 630 POKE VB+(CY-1)*40+CX-1,NCHAR:RETURN 700 REM ---- UNCOVER FIELD, USING FX, FY, SPCLEFT AND ADDINV 710 REM UNCOVER ZERO-NOTED SURROUNDING RECURSIVELY 720 F=PEEK(VB+(FY-1)*40+FX-1):IF F<>BKCHR AND F<>BKCHR+128 THEN RETURN 730 SPCLEFT=SPCLEFT-1 740 REM MINE COUNT ON SQUARE 750 MSQ=0:FOR X=0 TO 2:FOR Y=0 TO 2:MSQ=MSQ+M(FX+X,FY+Y):NEXT Y:NEXT X 760 IF MSQ>0 THEN POKE VB+(FY-1)*40+FX-1,ASC("0")-32+MSQ+ADDINV:RETURN 770 POKE VB+(FY-1)*40+FX-1, ASC(" ")-32+ADDINV 780 ADDINV=0 790 FOR X=-1 TO 1:FOR Y=-1 TO 1 800 INB=FX+X>=1 AND FX+X<=XSZ AND FY+Y>=1 AND FY+Y<=YSZ 810 F=-1:IF INB THEN F=PEEK(VB+(FY+Y-1)*40+FX+X-1) 820 IF F=BKCHR THEN UFSTACKX(UFSTACKP)=FX+X:UFSTACKY(UFSTACKP)=FY+Y:UFSTACKP=UFSTACKP+1 830 NEXT Y:NEXT X 840 IF UFSTACKP=1 THEN RETURN 850 UFSTACKP=UFSTACKP-1:FX=UFSTACKX(UFSTACKP):FY=UFSTACKY(UFSTACKP):GOSUB 700 860 GOTO 840
The above code uses GOSUB recursively (which uses stack for place of return), but all variables are global rather than being on the automatic stack. A stack is maintained manually. Positions to explore can appear on the stack repeatedly, a suboptimality. The above is slow but still more comfortable for the player than having to explore the affected fields manually.
An assignment: expand the above code with a solver that is going to automatically mark at least some fields where the presence of a mine is certain, to be called when the user presses S key. For instance, if the field says 1, there are 7 non-mine explored fields around it and there is one unexplored field around it not marked with a mine, mark that field with a mine.
A challenge: modify the code to use character mode 2, which allows different text colors for different characters (4 text colors + background color), where one pixel of the character is 2x2 the pixel of character mode 0. Use different colors for different purposes to increase usability; for instance, let the numbers have color #1, let the unexamined field have color #2 and let the mine mark have color #3. (In this mode, there are no inverse characters to be used for the cursor; one can use e.g. caret for cursor and keep the field state under the cursor in a separate variable.)
A challenge: rewrite the code to use character mode 12 (XL machines only), which allows 4 colors per character at the cost of lower resolution, while still allowing 40x24 characters on screen. Use custom character set. Use a PMG/sprite for cursor. This will enable a more colorful experience and probably a better usability. On the other hand, the code will be not so elegantly short. And one will have to think not only of programming but also of graphical design of the characters, and encode the character design in decimal numbers. This exercise is an excuse to learn about how to redefine a character set (including about how pairs of bits map to pixels and their colors) and how to use PMG/sprites.
Generate maze
editAssignment: Use the algorithm from Rosetta Code to generate a maze. Thus, consider a matrix of free square spaces all separated by walls. Randomly start at one such free space. Consider all four neighbors in random order and for all not yet visited ones, remove the wall facing them, mark them as visited and add them to a processing list implemented as a stack. Continue until the stack is empty. Handle corner cases and details not described here to make it work. Use the character mode 0 (40x24 characters).
A solution:
5 MX=39:MY=23 10 DIM F(MX,MY),V(MX,MY),SX(MX*MY),SY(MX*MY),C(4) 20 REM FIELD,VISITED,STACK X,STACK Y 30 SP=0:REM STACK POINTER 40 FOR X=1 TO MX:FOR Y=1 TO MY:F(X,Y)=1:V(X,Y)=0:NEXT Y:NEXT X 50 FOR X=2 TO MX STEP 2:FOR Y=2 TO MY STEP 2:F(X,Y)=0:NEXT Y:NEXT X 60 P=PEEK(560)+PEEK(561)*256 70 VB=PEEK(P+4)+PEEK(P+5)*256:REM VIDEOBASE 80 X=INT(RND(0)*(MX-1)/2)*2+2:Y=INT(RND(0)*(MY-1)/2)*2+2 90 SP=SP+1:SX(SP)=X:SY(SP)=Y 100 X=SX(SP):Y=SY(SP):SP=SP-1 110 V(X,Y)=1 120 C(1)=INT(RND(0)*4)+1:REM SHUFFLE 130 C(2)=INT(RND(0)*4)+1:IF C(1)=C(2) THEN GOTO 130 140 C(3)=INT(RND(0)*4)+1:IF C(3)=C(1) OR C(3)=C(2) THEN GOTO 140 150 FOR I=1 TO 4: IF I<>C(1) AND I<>C(2) AND I<>C(3) THEN C(4)=I:I=4 160 NEXT I 180 FOR I=1 TO 4 190 C1=C(I) 200 IF C1=1 AND X+2<MX THEN IF V(X+2,Y)=0 THEN V(X+2,Y)=1:F(X+1,Y)=0:SP=SP+1:SX(SP)=X+2:SY(SP)=Y 210 IF C1=2 AND X-2>1 THEN IF V(X-2,Y)=0 THEN V(X-2,Y)=1:F(X-1,Y)=0:SP=SP+1:SX(SP)=X-2:SY(SP)=Y 220 IF C1=3 AND Y+2<MY THEN IF V(X,Y+2)=0 THEN V(X,Y+2)=1:F(X,Y+1)=0:SP=SP+1:SX(SP)=X:SY(SP)=Y+2 230 IF C1=4 AND Y-2>1 THEN IF V(X,Y-2)=0 THEN V(X,Y-2)=1:F(X,Y-1)=0:SP=SP+1:SX(SP)=X:SY(SP)=Y-2 240 NEXT I 250 PRINT "STACK SIZE: ";SP 260 IF SP>0 THEN GOTO 100 270 REM ---- OUTPUT MAZE 280 PRINT CHR$(125) 290 FOR X=1 TO 39:FOR Y=1 TO 23 300 C=ASC("#")-32:IF F(X,Y)=0 THEN C=ASC(" ")-32 310 POKE VB+(Y-1)*40+X-1,C 320 NEXT Y:NEXT X 330 IF STRIG(0)=1 THEN GOTO 330 340 END
Notes on the implementation: The spaces are the locations with both x and y even and the walls are the other locations.
An exercise: modify the above code to plot the maze using graphics mode 7 (160x80 pixels). Modify the code further to support as big a maze as graphics mode 7 and Atari 800 XL memory allow.
A solution:
1 GRAPHICS 7 5 MX=159:MY=79:MSTSZ=1200:MXH=(MX-1)/2 10 DIM F$(MX*MY),V$((MX-1)/2*(MY-1)/2),C(4) 15 STACKSZ=INT(MX/2*MY/2):IF STACKSZ>MSTSZ THEN STACKSZ=MSTSZ 20 DIM SX(STACKSZ),SY(STACKSZ) 25 REM FIELD,VISITED,STACK X,STACK Y 30 SP=0:REM STACK POINTER 40 FOR X=1 TO MX:FOR Y=1 TO MY:F$(X+(Y-1)*MX,X+(Y-1)*MX)="1":NEXT Y:NEXT X 50 FOR X=2 TO MX STEP 2:FOR Y=2 TO MY STEP 2 55 F$(X+(Y-1)*MX,X+(Y-1)*MX)="0":V$(X/2+(Y/2-1)*MXH,X/2+(Y/2-1)*MXH)="0" 56 NEXT Y:NEXT X 60 P=PEEK(560)+PEEK(561)*256 70 VB=PEEK(P+4)+PEEK(P+5)*256:REM VIDEOBASE 80 X=INT(RND(0)*(MX-1)/2)*2+2:Y=INT(RND(0)*(MY-1)/2)*2+2 90 SP=SP+1:SX(SP)=X:SY(SP)=Y 100 X=SX(SP):Y=SY(SP):SP=SP-1 105 XH=X/2:YH=Y/2 110 V$(XH+(YH-1)*MXH,XH+(YH-1)*MXH)="1" 120 C(1)=INT(RND(0)*4)+1:REM SHUFFLE 130 C(2)=INT(RND(0)*4)+1:IF C(1)=C(2) THEN GOTO 130 140 C(3)=INT(RND(0)*4)+1:IF C(3)=C(1) OR C(3)=C(2) THEN GOTO 140 150 FOR I=1 TO 4: IF I<>C(1) AND I<>C(2) AND I<>C(3) THEN C(4)=I:I=4 160 NEXT I 170 IF SP+4>STACKSZ THEN PRINT "OUT OF STACK";:END 180 FOR I=1 TO 4 190 C1=C(I) 200 IF C1=1 AND X+2<MX THEN IF V$(XH+1+(YH-1)*MXH,XH+1+(YH-1)*MXH)="0" THEN GOTO 205 201 GOTO 210 205 V$(XH+1+(YH-1)*MXH,XH+1+(YH-1)*MXH)="1":F$(X+1+(Y-1)*MX,X+1+(Y-1)*MX)="0":SP=SP+1:SX(SP)=X+2:SY(SP)=Y 210 IF C1=2 AND X-2>1 THEN IF V$(XH-1+(YH-1)*MXH,XH-1+(YH-1)*MXH)="0" THEN GOTO 215 211 GOTO 220 215 V$(XH-1+(YH-1)*MXH,XH-1+(YH-1)*MXH)="1":F$(X-1+(Y-1)*MX,X-1+(Y-1)*MX)="0":SP=SP+1:SX(SP)=X-2:SY(SP)=Y 220 IF C1=3 AND Y+2<MY THEN IF V$(XH+YH*MXH,XH+YH*MXH)="0" THEN GOTO 225 221 GOTO 230 225 V$(XH+YH*MXH,XH+YH*MXH)="1":F$(X+Y*MX,X+Y*MX)="0":SP=SP+1:SX(SP)=X:SY(SP)=Y+2 230 IF C1=4 AND Y-2>1 THEN IF V$(XH+(YH-2)*MXH,XH+(YH-2)*MXH)="0" THEN GOTO 235 231 GOTO 240 235 V$(XH+(YH-2)*MXH,XH+(YH-2)*MXH)="1":F$(X+(Y-2)*MX,X+(Y-2)*MX)="0":SP=SP+1:SX(SP)=X:SY(SP)=Y-2 240 NEXT I 250 IF SP/10=INT(SP/10) THEN PRINT "STACK SIZE: ";SP 260 IF SP>0 THEN GOTO 100 270 REM ---- OUTPUT MAZE 290 FOR X=1 TO MX:FOR Y=1 TO MY 300 COLOR 1:IF F$(X+(Y-1)*MX,X+(Y-1)*MX)="0" THEN COLOR 0 310 PLOT X-1, Y-1 320 NEXT Y:NEXT X 330 END
Implementation notes: we use a string as a byte array for the field array and visited array, dropping the memory requirements for them by the factor of 6. As a result, we need to calculate the physical index for the 2-dimensional array manually, and we need to repeate the index.
The above code makes full use of mode 7 screen size. Switching the above to mode 8 runs out of memory on Atari 800 XL: mode 8 needs twice as much video memory as mode 7. One could support mode 8 by encoding the boolean information in bits rather than bytes, but given the lack of bit manipulation operations in Atari BASIC, the result would be intolerably slow.
Reconstructing floating-point numbers
editAssignment: Using the understanding we gained in section Floating point numbers, write code to reconstruct a floating-point number from its internal representation, knowing that variable base is stored at addresses 134 and 135 and that the first variable's data start at offset 2. Test the reconstruction by randomly generating values to be reconstructed. This is a little exercise in systematic automated verification of an engineering hypothesis.
10 A=0:VB=PEEK(134)+256*PEEK(135):REM VARIABLE BASE 15 REM ---- RANDOM IN [0,1) PLUS SIGN 20 FOR I=1 TO 10 30 A=RND(0) 35 S=INT(RND(0)*2):IF S=1 THEN A=-1*A 40 GOSUB 500 50 PRINT B,A=B 55 IFA <>B THEN PRINT "FAIL":END 60 NEXT I 65 REM ---- LARGE RANDOM PLUS SIGN 70 FOR I=1 TO 100 80 A=RND(0) 85 S=INT(RND(0)*2):IF S=1 THEN A=-1*A 87 E=INT(RND(0)*(98+98))-98 88 A=A*(10^E) 90 GOSUB 500 100 PRINT B,A=B 110 IFA<>B THEN PRINT "FAIL":END 120 NEXT I 130 REM ---- RANDOM VIA INTERNALS 135 FOR I=1 TO 100 140 FOR OFF=3 TO 7 150 POKE VB+OFF, INT(RND(0)*10)*16+INT(RND(0)*10) 160 NEXT OFF 170 S=INT(RND(0)*2) 180 E=INT(RND(0)*(49+49))-49 190 POKE VB+2, 128*S+(E+64) 200 GOSUB 500 210 PRINT B,A=B 220 NEXT I 230 END 500 REM --- RECONSTRUCT A IN B 510 B=0 520 FOR OFF=3 TO 7 530 V=PEEK(VB+OFF) 540 V=V-6*INT(V/16) 550 B=B+V 560 IF OFF<7 THEN B=100*B 570 NEXT OFF 580 FB=PEEK(VB+2) 590 IF FB>=128 THEN B=-1*B:FB=FB-128 600 E=FB-64-4 610 IF E>0 THEN FOR RI=1 TO E:B=B*100:NEXT RI 620 IF E<0 THEN FOR RI=1 TO ABS(E):B=B/100:NEXT RI 630 RETURN
Note: One would think that exponentiation of 100 could be done using the ^ operator. But it is not so: the operator leads to loss of precision for high exponents.
Links:
- The Atari BASIC Source Book. 1983. https://archive.org/details/ataribooks-the-atari-basic-source-book. -- chapter Variable Values
Assembly-style integer multiplication
editAssignment: implement positive integer multiplication in the style of 6502 assembly. Thus, use only operations that have direct 6502 assembly equivalent, including multiplication by 2 (arithmetic shift left), addition and testing for inequality. Do not use general multiplication ("*"): it has no direct equivalent. As a simplification, do no bother about the 8-bit value limitations, and thus, pretend you have a many-bit integer multiplication by 2 rather than only 8-bit one. You can use the bitwise OR implementation from another exercise to create bitwise AND. The point: figure out or recall the algorithm that would be implemented in assembly, without having to write assembly. The algorithm is a simplified version of the algorithm for decimal multiplication learned in school.
A solution:
10 PRINT "X: ";:INPUT X 20 PRINT "Y: ";:INPUT Y 30 LMX=X:LMY=Y:GOSUB 300 40 PRINT "RES: ";LMRES 50 PRINT "X*Y: ";X*Y 60 END 300 REM ---- LOW-LEVEL MULTIPLICATION 310 LMYI=LMY 320 BITMASK=1 330 LMRES=0 340 IF LMX<BITMASK THEN RETURN 350 BAX=LMX:BAY=BITMASK:GOSUB 400:REM BITAND 360 IF BARES THEN LMRES=LMRES+LMYI 370 BITMASK=BITMASK*2 380 LMYI=LMYI*2 390 GOTO 340 400 REM ---- BITWISE AND, 32 BITS 410 BARES=0 420 BAL=2147483640+8:REM 2^31 WITH FULL 10-DIGIT PRECISION 430 FOR J=0 TO 31 440 BARES=2*BARES 450 IF BAX>=BAL AND BAY>=BAL THEN BARES=BARES+1 460 IF BAX>=BAL THEN BAX=BAX-BAL 470 IF BAY>=BAL THEN BAY=BAY-BAL 480 BAX=BAX*2: BAY=BAY*2 490 NEXT J 495 RETURN
Compared to the builtin multiplication, one limitation of the above is that the bitwise AND only handles 32-bit input values. For 999999999*999999999, the above is a little less precise than the builtin.
String integer multiplication
editAssignment: given two input strings indicating positive integers in decimal, calculate an output string containing the result of multiplication.
A solution, arrived at by first writing and testing support procedures:
5 PR=100 10 DIM AA$(PR),AB$(PR),AC$(PR+1) 20 DIM DMA$(PR),DMD$(1),DMC$(PR+1) 30 DIM A$(PR),B$(PR),C$(PR) 40 INPUT A$:INPUT B$:GOSUB 700:PRINT C$ 50 END 400 REM ---- PLUS: IN: AA$, AB$; OUT: AC$ 405 L=LEN(AA$):IF LEN(AB$)>L THEN L=LEN(AB$) 410 AC$="0":AC$(L+1-1)="0":AC$(2)=AC$ 420 IF NOT (LEN(AA$)>LEN(AB$)) THEN GOTO 450 430 ABL=LEN(AB$) 435 FOR I=ABL TO 1 STEP -1:AB$(I+L-ABL,I+L-ABL)=AB$(I,I):NEXT I 440 FOR I=1 TO LEN(AA$)-ABL:AB$(I,I)="0":NEXT I 450 IF NOT (LEN(AB$)>LEN(AA$)) THEN GOTO 480 460 AAL=LEN(AA$) 465 FOR I=AAL TO 1 STEP -1:AA$(I+L-AAL,I+L-AAL)=AA$(I,I):NEXT I 470 FOR I=1 TO LEN(AB$)-AAL:AA$(I,I)="0":NEXT I 480 OF=0 490 FOR I=L TO 1 STEP -1 500 DR=ASC(AA$(I,I))-48+ASC(AB$(I,I))-48+OF 510 OF=0:IF DR>=10 THEN OF=1:DR=DR-10 520 AC$((LEN(AC$)-L)+I,(LEN(AC$)-L)+I)=CHR$(DR+48) 530 NEXT I 540 IF OF THEN AC$((LEN(AC$)-L),(LEN(AC$)-L))=CHR$(OF+48) 550 IF AC$(1,1)="0" THEN AC$(1)=AC$(2) 560 RETURN 600 REM ---- MULT BY DIGIT: IN: DMA$, DMD$; OUT: DMC$ 610 DMC$="0":DMC$(LEN(DMA$)+1-1)="0":DMC$(2)=DMC$ 615 OF=0 620 FOR I=LEN(DMA$) TO 1 STEP -1 630 DR=(ASC(DMA$(I,I))-48)*(ASC(DMD$(1,1))-48)+OF 640 OF=INT(DR/10):L=DR-OF*10 650 DMC$(I+1,I+1)=CHR$(L+48) 660 NEXT I 670 IF OF THEN DMC$(1,1)=CHR$(OF+48) 680 IF DMC$(1,1)="0" THEN DMC$(1)=DMC$(2) 690 RETURN 700 REM ---- MULT; IN: A$, B$; OUT: C$ 705 C$="0" 710 FOR J=LEN(B$) TO 1 STEP -1 720 DMA$=A$:DMD$=B$(J,J):GOSUB 600:REM DMC$=A$*B$(J,J) 730 IF J<LEN(B$) THEN FOR K=1 TO LEN(B$)-J:DMC$(LEN(DMC$)+1)="0":NEXT K 740 AA$=C$:AB$=DMC$:GOSUB 400:C$=AC$ 750 NEXT J 760 RETURN
Other considerations
editPerformance
editAtari BASIC is hugely slower compared to 6502 assembly. According to various sources, Frank Ostrowski's Turbo-BASIC XL is much faster. The causes of the slowness of Atari BASIC compared to assembly include its interpreted nature, its lack of integral arithmetic (all is floating point), and other.
We can compare the speed of Atari BASIC to assembly by considering the task of clearing the character mode screen, which amounts to zeroing the video memory. The speed of assembly for this task is given by printing CHR$(125), equivalent to CLS in some other BASICs.
10 GOSUB 500:OTIME=TIME 20 CLSITERCOUNT=200:CLSITER2COUNT=1 30 FOR I=1 TO CLSITERCOUNT:PRINT CHR$(125):NEXT I 40 GOSUB 500:T1=TIME-OTIME:OTIME=TIME 50 P=PEEK(560)+PEEK(561)*256 60 VB=PEEK(P+4)+PEEK(P+5)*256:REM VIDEOBASE 70 FOR I=1 TO CLSITER2COUNT 80 FOR J=0 TO 40*24-1: POKE VB+J, 0:NEXT J 90 NEXT I 100 GOSUB 500:T2=TIME-OTIME 105 PRINT CHR$(125) 110 PRINT "ASM FRAMES: ";T1;" (ITERS: ";CLSITERCOUNT;")" 120 PRINT "BASIC FRAMES: ";T2;" (ITERS: ";CLSITER2COUNT;")" 130 PRINT "BASIC/ASM RATIO: ";(T2*CLSITERCOUNT)/(T1*CLSITER2COUNT) 140 END 500 TIME=PEEK(18)*65536+PEEK(19)*256+PEEK(20):RETURN
The above test experimentally produces the performance ratio of over 400 times in favor of assembly, and that even counts the overhead of the loop around chr$(125).
We can make a similar comparison for fast string filling vs. manual one:
5 SLEN=1000 10 DIM S$(SLEN) 20 ITERCOUNT=200:ITERCOUNT2=1 30 GOSUB 500:OTIME=TIME 40 FOR I=1 TO ITERCOUNT 50 S$="0":S$(SLEN-1)="0":S$(2)=S$:REM FAST INIT 60 NEXT I 70 GOSUB 500:T1=TIME-OTIME:OTIME=TIME 80 FOR I=1 TO ITERCOUNT2 90 FOR J=1 TO SLEN:S$(J,J)="0":NEXT J 100 NEXT I 110 GOSUB 500:T2=TIME-OTIME 120 PRINT "ASM FRAMES: ";T1;" (ITERS: ";ITERCOUNT;")" 130 PRINT "BASIC FRAMES: ";T2;" (ITERS: ";ITERCOUNT2;")" 140 PRINT "BASIC/ASM RATIO: ";(T2*ITERCOUNT)/(T1*ITERCOUNT2) 150 END 500 TIME=PEEK(18)*65536+PEEK(19)*256+PEEK(20):RETURN
The above test experimentally produces the performance ratio of over 300 times in favor of assembly. Changing SLEN to 10000 results in a ratio of over 500.
Links:
- Atari BASIC Faster and Better, archive.org
Vintage BASIC
editVintage BASIC is a FOSS implementation of an 8-bit-style BASIC. The web site features BASIC sources for many games. One can try to make them work with Atari BASIC, which requires some adaptation. One problem is that PRINT TAB(...) does not work. Another one is that Atari BASIC requires string to be dimensioned. Another one is that Atari BASIC's INPUT does not support prompt string and a separate PRINT is required instead.
Links:
- Games, vintage-basic.net
- Vintage BASIC User's Guide, vintage-basic.net
Altirra BASIC
editAltirra BASIC is a reimplementation of Atari BASIC, with additional features. Its reference manual can be used as a guide to Atari BASIC as long as one pays attention to the differences, which are indicated in section "Compatibility with Atari BASIC".
Links:
- Altirra BASIC Reference Manual, virtualdub.org
Rosetta Code
editOne can use Rosetta Code as a source of exercises. Most lack a solution for Atari BASIC there. Exercises/assignments fit for Atari BASIC are likely to include those that have a solution for Commodore BASIC for Commodore 64, of which there are many more. Other categories of interest in Rosetta Code may be those for BBC BASIC and Applesoft BASIC.
Links:
- Category:Commodore BASIC, rosettacode.org
References
edit- ↑ Atari BASIC, ataricompendium.com
Further reading
edit- Atari BASIC, wikipedia.org
- ATASCII, wikipedia.org
- Atari BASIC Reference Manual, archive.org
- Atari BASIC Quick Reference Guide, atariwiki.org
- Understanding Atari graphics, atarimania.com
- Atari Graphics & Arcade Game Design by Jeffrey Stanton with Dan Pinal, atariarchives.org
- Category:Atari BASIC, rosettacode.org
- Atari 8-bit, emulation.gametechwiki.com
- Atari Basic instructions, blog.3b2.sk -- short descriptions of instructions
- Atari BASIC Source book, atariwiki.org -- seems to lack the source itself
- The Atari BASIC Source Book, 1983, archive.org -- includes the assembly source code for Atari BASIC
- Atari BASIC, atariwiki.org