Atari BASIC programming

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

edit

Atari 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

edit

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

edit

Getting started

edit

Programs 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

edit
10 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

edit

There 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

edit

A 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

edit

One 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

edit

Strings 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

edit

The 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:

Mathematics

edit

Mathematical 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

edit

In 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:

Player-missile graphics

edit

Player-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:

Reporting joystick state/events

edit
10 PRINT STICK(0), STRIG(0)
20 GOTO 10

Clearing the screen

edit

There is no CLS. One can PRINT CHR$(125) instead.

Abbreviations

edit

Abbreviations include:

  • "?" for PRINT.
  • L. for LIST
  • GR. for GRAPHICS
  • SE. for SETCOLOR
  • Etc.

Links:

Memory map

edit

Memory map is useful to know the effects of various PEEKs and POKEs. See the link.

Links:

Measuring time

edit

Measuring 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:

Manipulating video memory

edit

There 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:

Calling assembly

edit

One 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:

Limitations

edit

Limitations 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

edit

Calculating factorial

edit
10 INPUT N
20 F=1
30 FOR I=1 TO N
40 F=F*I
50 NEXT I
60 PRINT F

Plotting diagonal lines

edit
10 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

edit
10 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

edit

The 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:

Plotting random pixels

edit

Plotting 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

edit

Outputting 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:

Plotting Mandelbrot set

edit

An 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

edit

Atari 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

edit

Plot 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

edit

Let 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

edit

Color 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:

Low-level access plotting

edit

Assignment: 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

edit

Byte-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:

Plotting joystick-controlled vehicle

edit

The 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

edit

Plotting 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

edit

We 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

edit

To 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

edit

Plotting 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

edit
10 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

edit

One 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

edit
10 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

edit

There 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:

Hangman

edit

The 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

edit

We 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

edit

Using 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

edit

Using 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

edit

We 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

edit

Assignment: 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

edit

Assignment 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

edit

Implementing 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

edit

Assignment: 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

edit

Assignment: 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:

Assembly-style integer multiplication

edit

Assignment: 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

edit

Assignment: 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

edit

Performance

edit

Atari 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:

Vintage BASIC

edit

Vintage 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:

Altirra BASIC

edit

Altirra 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:

Rosetta Code

edit

One 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:

References

edit
  1. Atari BASIC, ataricompendium.com

Further reading

edit