Samstag, Oktober 10, 2026

C64 Basic: Zufallszahlen mit dem RND-Befehl

Normalerweise suche ich mir für jedes Video aus meiner Reihe C64 BASIC-Befehle zuerst den Befehl aus und baue dann ein passendes Intro dazu. Diesmal lief es umgekehrt: Ich hatte die Idee, mein Logo Zeile für Zeile aufzubauen, und zwar so, dass die einzelnen Blöcke innerhalb einer Zeile in zufälliger Reihenfolge erscheinen. Dafür brauchte ich Zufall – und damit den Befehl RND.

Kann ein Computer Zufall?

Streng genommen nicht. Ein Computer soll sich vorhersehbar verhalten, genau dafür ist er gebaut. Was wir auf dem C64 (und auf jedem anderen Rechner) als Zufall bekommen, wird von einem Algorithmus berechnet: Aus einem Startwert entsteht nach festen Regeln eine Zahlenfolge, die zufällig aussieht. Wer den Startwert und den Algorithmus kennt, kann die Folge jederzeit exakt reproduzieren.

Man nennt das Pseudozufall. Für Spiele, Grafikeffekte oder ein Intro reicht das völlig aus.

Die Syntax des RND-Befehls

X = RND(n)

RND liefert immer eine Fließkommazahl größer oder gleich 0 und kleiner als 1. Was dabei herauskommt, hängt vom Vorzeichen des Parameters n ab:

  • n positiv (z. B. RND(1) oder RND(5)): RND berechnet aus dem zuletzt erzeugten Wert den nächsten Wert der Folge. Der konkrete Wert von n spielt dabei keine Rolle, RND(5) und RND(1) verhalten sich gleich. Jeder Aufruf liefert eine neue Zahl.
  • n negativ (z. B. RND(-23)): Die Zahl wird als neuer Startwert verwendet. Derselbe negative Wert liefert immer dasselbe Ergebnis, und auch alle folgenden RND(1)-Aufrufe ergeben danach wieder dieselbe Folge.
  • n = 0: Der C64 liest Zählerstände aus dem CIA-Baustein aus und baut daraus die Zahl zusammen. Weil diese Zähler ständig laufen, kommt bei jedem Aufruf etwas anderes heraus.

Ein Detail zu den negativen Werten: RND(-23) liefert eine sehr kleine Zahl, rund 4,28 · 10⁻⁸. Der C64 zeigt sie als 4.28…E-08 an. Man übersieht das „E-08” am Ende leicht und hält die Zahl dann für 4,28. Bei kleinen negativen Startwerten ist der erste Wert also praktisch 0 – erst die folgenden RND(1)-Aufrufe liefern brauchbare Zahlen.

Dass derselbe negative Startwert immer dasselbe Ergebnis bringt, ist kein Fehler, sondern manchmal genau das, was man will: etwa wenn ein Spiel bei jedem Start dasselbe „zufällige” Level erzeugen soll.

Beispiel: Würfeln

Das klassische Beispiel ist ein Würfelspiel. Ein Würfel soll ganze Zahlen von 1 bis 6 liefern. Dafür sind zwei Schritte nötig:

  1. RND(0)*6 ergibt eine Zahl von 0 bis knapp unter 6, z. B. 1,523.
  2. INT(…) schneidet die Nachkommastellen ab (es rundet nicht!). Aus 1,523 wird 1. Wir bekommen also ganze Zahlen von 0 bis 5.
  3. +1 verschiebt den Bereich auf 1 bis 6.

Die allgemeine Form lautet:

X = INT(RND(1)*ANZAHL)+KLEINSTER_WERT

Für einen Becher mit 80 Würfeln, also eine Bildschirmseite voll:

10 PRINT CHR$(147)
20 FOR I=1 TO 80
30 PRINT INT(RND(0)*6)+1,
40 NEXT

Das Komma am Ende von Zeile 30 sorgt dafür, dass die Werte in vier Spalten nebeneinander ausgegeben werden. Alle Zahlen von 1 bis 6 sind vertreten.

Das Rätsel um die 300

Im Video habe ich dann einen 300-seitigen Würfel simuliert – auf dem Computer geht das ja. Um zu prüfen, ob die 300 überhaupt vorkommt, gibt das Programm nur Treffer aus:

10 FOR I=1 TO 1500
20 X=INT(RND(0)*300)+1
30 IF X=300 THEN PRINT X
40 NEXT

Ergebnis: Die 300 kam weder bei 800 noch bei 1.500 noch bei 5.000 Durchläufen vor. Die 299 dagegen schon. Mit *301 statt *300 tauchte die 300 sofort auf. Beim Herumprobieren zeigte sich: Bis 256 wird der Höchstwert erreicht, darüber nicht mehr. Im Video konnte ich mir das nicht erklären.

Die Erklärung liegt in der Art, wie RND(0) seine Zahl zusammensetzt. Das BASIC-ROM liest vier Bytes aus dem CIA-Baustein und legt sie direkt in die Mantisse der Fließkommazahl:

  • Das höchstwertige Byte ist das Low-Byte von Timer A, also ein Wert von 0 bis 255.
  • Danach folgen die Zehntelsekunden der CIA-Uhr (nur 0 bis 9), das High-Byte von Timer A und die Sekunden der Uhr.

Das höchstwertige Byte bestimmt den Wert grob in Schritten von 1/256. Der größte erreichbare Wert ist deshalb 255/256 plus einen sehr kleinen Rest aus den anderen Bytes, also etwa 0,9961.

Rechnen wir nach:

  • Bei 256: 255/256 · 256 = 255, INT ergibt 255, plus 1 ergibt 256. Der Höchstwert wird erreicht.
  • Bei 300: 255/256 · 300 ≈ 298,8, INT ergibt 298, plus 1 ergibt 299. Die 300 ist unerreichbar.

Die magische Grenze bei 256 ist also kein Zufall, sondern folgt direkt aus dem einen Byte, das den Wert von RND(0) bestimmt. (Je nachdem, ob die Uhr im CIA gerade läuft, kann sich die Grenze um ein paar Werte nach oben verschieben.)

Das hat noch eine zweite Folge: RND(0) kennt im Grunde nur 256 verschiedene grobe Werte, und in einer Schleife mit immer gleicher Laufzeit fragt es den Timer in einem festen Rhythmus ab. Daher kann die Verteilung ungleichmäßig ausfallen – im Video kam zum Beispiel die 6 auffällig oft vor.

Die Lösung: RND(0) nur einmal zum Setzen des Startwerts verwenden und danach mit RND(1) arbeiten. RND(1) berechnet den nächsten Wert mit dem eigentlichen Zufallsalgorithmus und nutzt alle Bytes der Mantisse. Damit sind alle Werte von 1 bis 300 erreichbar.

Startwert über TI setzen

Eine verbreitete Empfehlung ist, den Startwert am Programmanfang einmal aus der Systemuhr TI zu holen. TI zählt seit dem Einschalten in 1/60-Sekunden hoch, und weil man nie genau zur gleichen Zeit startet, bekommt man bei jedem Programmstart eine andere Folge:

10 X=RND(-TI)
20 FOR I=1 TO 1500
30 X=INT(RND(1)*300)+1
40 IF X=300 THEN PRINT X
50 NEXT

Mit dieser Variante taucht auch die 300 auf.

RND(-TI) gehört an den Anfang des Programms und nicht in die Schleife: TI ändert sich nur alle 1/60 Sekunde. Bei jedem Durchlauf neu gesetzt, würde der Startwert oft gleich bleiben, und es kämen viele gleiche Zahlen hintereinander heraus.

Das Intro

TRJ-RND.png

Jetzt zum Intro. Hier sorgt RND dafür, dass sich jede Zeile meines Logos Block für Block in zufälliger Reihenfolge aufbaut. Im Video lief das Intro als compilierte Fassung und war deshalb deutlich schneller. Das BASIC-Original zeigt aber besser, was passiert.

Der Trick: invertierte Sprites

Das Logo „RETRO JOURNAL” besteht aus sieben Sprites. Wer sich mit Sprites auskennt, wird stutzen: Hi-Res-Sprites haben nur eine Farbe und können ihre Farbe nicht zeilenweise wechseln. Die Buchstaben im Intro sind aber mehrfarbig und die Farben laufen sogar durch.

Die Lösung: Die Sprites sind schwarz und invertiert. Gesetzt sind nur die Pixel um die Buchstaben herum, die Buchstaben selbst sind transparent. Da Bildschirmrand und Hintergrund ebenfalls schwarz sind, sieht man von den Sprites selbst nichts. Durch die transparenten Stellen scheint aber das durch, was dahinter auf dem Bildschirm steht – und dort liegen farbige Balken aus inversen Leerzeichen.

Setzt man die Sprite-Farbe testweise von 0 (Schwarz) auf 1 (Weiß), fliegt der Trick sofort auf: Man sieht dann die weißen Rahmen der Sprites mit den ausgesparten Buchstaben.

Dieselbe Technik verwenden viele Demo- und Intro-Coder, oft mit Rasterbars oder einem Bild hinter den Sprites. Das Prinzip ist immer das gleiche: Der C64 kann das eigentlich nicht – es sieht nur so aus.

Der zufällige Aufbau

Hinter den Sprites liegen fünf Bildschirmzeilen. Für jede Zeile läuft dieselbe Schleife (hier für die erste Zeile):

530 SW=RND(0)
540 ZZ=INT(RND(SW)*40)
550 IF PEEK(1384+ZZ)=160 THEN GOTO 530
560 POKE 1384+ZZ,160
570 POKE 55656+ZZ,11
580 F%=F%+1:IF F%=40 THEN F%=0:GOTO 600
590 GOTO 530
  • Zeile 530/540: RND(0) holt einen Startwert aus dem Timer, RND(SW) berechnet daraus die zufällige Spalte ZZ von 0 bis 39. Da SW positiv ist, wirkt RND(SW) wie RND(1) – es ist also genau die Kombination, die oben als Lösung beschrieben ist.
  • Zeile 550: Steht an dieser Position schon ein inverses Leerzeichen (Bildschirmcode 160), wird eine neue Spalte gewürfelt.
  • Zeile 560/570: Sonst wird das Zeichen in den Bildschirmspeicher gesetzt und im Farb-RAM eingefärbt.
  • Zeile 580: F% zählt die gesetzten Blöcke. Nach 40 Blöcken ist die Zeile voll und es geht mit der nächsten weiter.

Die fünf Zeilen bekommen die Farben 11, 12, 15, 12 und 11 (Dunkelgrau, Grau, Hellgrau, Grau, Dunkelgrau). So entsteht ein Verlauf, der hinter dem Logo wie eine Wölbung wirkt.

Gegen Ende wird die Suche nach einer freien Spalte langsamer, weil RND immer häufiger eine bereits belegte Position trifft. Beim letzten freien Block liegt die Trefferchance nur noch bei 1 zu 40.

Die laufenden Farben

Ab Zeile 900 werden nur noch die Werte im Farb-RAM geändert. Eine Schleife läuft spaltenweise über die fünf Zeilen und färbt sie mit einem neuen Verlauf ein, z. B. 6/14/1/14/6 (Blau, Hellblau, Weiß, Hellblau, Blau). Danach läuft ein roter Verlauf von rechts nach links, dann ein grüner und ein gelb-brauner. Mit GOTO 900 beginnt das Ganze von vorn. Die Schleife über X in Zeile 906 lässt nebenbei die drei Buchstaben „RND” im Untertitel in wechselnden Farben blinken.

Weitere Details im Listing

  • Zeile 70: Beim Einlesen der Sprite-Daten wird jeder Wert auch in die Rahmenfarbe gepoked. Das ergibt das Flackern im Rahmen während des Erstellens der Sprites.
  • Zeile 100–400: Die Sprite-Zeiger 200 bis 206 zeigen auf die Adressen 12800 bis 13247 (200 · 64 = 12800), wo Zeile 70 die Daten ablegt.
  • Zeile 355: POKE 53264,96 setzt das neunte Bit der X-Position für die Sprites 5 und 6 (32 + 64 = 96), damit sie rechts von Position 255 stehen können.
  • Zeile 440/450: Alle Sprites sind in X- und Y-Richtung doppelt vergrößert.

Das komplette Listing

Hinweis: {down} steht für die Cursor-unten-Taste in einem String. {40 x CBM-@} bedeutet 40-mal das Unterstrich-Grafikzeichen (Commodore-Taste + @).

10 PRINT CHR$(147):PRINT CHR$(14):PRINT CHR$(152)
20 POKE53280,0:POKE 53281,0
40 POKE 53285,1: REM MULTICOLOR 1
50 POKE 53286,6: REM MULTICOLOR 2
60 POKE 53269,255 : REM SET ALL 8 SPRITES VISIBLE
70 FOR X=12800 TO 12800+447: READ Y: POKE X,Y: POKE 53280,Y:NEXT X
71 PRINT”{down}{down}{down}TOBONOCOM’S”
72 PRINT”{40 x CBM-@}”;
80 :: REM SPRITE_0
90 POKE 53287,0: REM COLOR = 0
100 POKE 2040,200: REM POINTER
110 POKE 53248, 24: REM X POS
120 POKE 53249, 120: REM Y POS
130 :: REM SPRITE_9
140 POKE 53288,0: REM COLOR = 0
150 POKE 2041,201: REM POINTER
160 POKE 53250, 72: REM X POS
170 POKE 53251, 120: REM Y POS
180 :: REM SPRITE_10
190 POKE 53289,0: REM COLOR = 0
200 POKE 2042,202: REM POINTER
210 POKE 53252, 120: REM X POS
220 POKE 53253, 120: REM Y POS
230 :: REM SPRITE_11
240 POKE 53290,0: REM COLOR = 0
250 POKE 2043,203: REM POINTER
260 POKE 53254, 168: REM X POS
270 POKE 53255, 120: REM Y POS
280 :: REM SPRITE_12
290 POKE 53291,0: REM COLOR = 0
300 POKE 2044,204: REM POINTER
310 POKE 53256, 216:REM X POS
320 POKE 53257, 120: REM Y POS
330 :: REM SPRITE_13
340 POKE 53292,0: REM COLOR = 0
350 POKE 2045,205: REM POINTER
355 POKE 53264,96
360 POKE 53258,8: REM X POS
370 POKE 53259, 120: REM Y POS
380 :: REM SPRITE_14
390 POKE 53293,0: REM COLOR = 0
400 POKE 2046,206: REM POINTER
410 POKE 53260, 56 : REM X POS
420 POKE 53261, 120: REM Y POS
430 POKE 53276, 0: REM MULTICOLOR
440 POKE 53277,255: REM WIDTH
450 POKE 53271,255: REM HEIGHT
500 F%=0
530 SW=RND(0)
540 ZZ=INT(RND(SW)*40)
550 IF PEEK(1384+ZZ)=160 THEN GOTO 530
560 POKE 1384+ZZ,160
570 POKE 55656+ZZ,11
580 F%=F%+1:IF F%=40 THEN F%=0:GOTO 600
590 GOTO 530
600 SW=RND(0)
610 ZZ=INT(RND(SW)*40)
620 IF PEEK(1424+ZZ)=160 THEN GOTO 600
630 POKE 1424+ZZ,160
640 POKE 55696+ZZ,12
650 F%=F%+1:IF F%=40 THEN F%=0:GOTO 670
660 GOTO 600
670 SW=RND(0)
680 ZZ=INT(RND(SW)*40)
690 IF PEEK(1464+ZZ)=160 THEN GOTO 670
700 POKE 1464+ZZ,160
710 POKE 55736+ZZ,15
720 F%=F%+1:IF F%=40 THEN F%=0:GOTO 740
730 GOTO 670
740 SW=RND(0)
750 ZZ=INT(RND(SW)*40)
760 IF PEEK(1504+ZZ)=160 THEN GOTO 740
770 POKE 1504+ZZ,160
780 POKE 55776+ZZ,12
790 F%=F%+1:IF F%=40 THEN F%=0:GOTO 810
800 GOTO 740
810 SW=RND(0)
820 ZZ=INT(RND(SW)*40)
830 IF PEEK(1544+ZZ)=160 THEN GOTO 810
840 POKE 1544+ZZ,160
850 POKE 55816+ZZ,11
860 F%=F%+1:IF F%=40 THEN F%=0:GOTO 875
870 GOTO 810
875 PRINT”{down}{down}{down}{down}{down}{down}{40 x CBM-@}”
880 PRINT”        C64 BASIC BEFEHLE NR.6”
885 PRINT
890 PRINT”               - RND -”
900 SP=55656
903 FOR I=1 TO 39
905 POKE SP,6
906 FOR X=0 TO 2:POKE 56033+X,I: NEXT X
910 POKE SP+40,14:POKE SP+80,1
915 POKE SP+120,14:POKE SP+160,6
920 SP = SP+1
925 POKE SP+160,6:POKE SP+120,14
930 POKE SP+80,1:POKE SP+40,14
935 POKE SP,6
940 NEXT I
944 SP=55695
945 FOR I=1 TO 39
946 FOR X=0 TO 2:POKE 56033+X,I: NEXT X
950 POKE SP,2
955 POKE SP+40,10:POKE SP+80,1
960 POKE SP+120,10:POKE SP+160,2
965 SP = SP-1
970 POKE SP+160,2:POKE SP+120,10
975 POKE SP+80,1:POKE SP+40,10
980 POKE SP,2
985 NEXT I
990 SP=55656
995 FOR I=1 TO 39
1000 POKE SP,5
1006 FOR X=0 TO 2:POKE 56033+X,I: NEXT X
1010 POKE SP+40,13:POKE SP+80,1
1015 POKE SP+120,13:POKE SP+160,5
1020 SP = SP+1
1025 POKE SP+160,5:POKE SP+120,13
1030 POKE SP+80,1:POKE SP+40,13
1035 POKE SP,5
1040 NEXT I
1044 SP=55695
1045 FOR I=1 TO 39
1050 POKE SP,9
1051 FOR X=0 TO 2:POKE 56033+X,I: NEXT X
1055 POKE SP+40,8:POKE SP+80,7
1060 POKE SP+120,8:POKE SP+160,9
1065 SP = SP-1
1070 POKE SP+160,9:POKE SP+120,8
1075 POKE SP+80,7:POKE SP+40,8
1080 POKE SP,9
1085 NEXT I
1095 GOTO 900
2000 :: REM SPRITE_0 / SINGLECOLOR / COLOR: 0
2010 DATA 255,255,255,128,56,0,130,24,0,130,24,0,130,8,0,130
2020 DATA 8,0,130,8,0,130,8,0,130,24,31,128,56,3,130,24
2030 DATA 3,130,24,3,130,8,3,130,8,31,130,8,0,130,8,0
2040 DATA 130,8,0,130,8,0,130,8,0,130,8,0,255,255,255,0
2050 :: REM SPRITE_9 / SINGLECOLOR / COLOR: 0
2060 DATA 255,255,255,128,8,3,128,8,33,128,8,33,128,8,32,128
2070 DATA 8,32,128,8,32,128,8,32,240,120,33,240,120,3,240,120
2080 DATA 35,240,120,33,240,120,33,240,120,32,240,120,32,240,120,32
2090 DATA 240,120,32,240,120,32,240,120,32,240,120,32,255,255,255,0
2100 :: REM SPRITE_10 / SINGLECOLOR / COLOR: 0
2110 DATA 255,255,255,128,15,255,130,15,255,130,15,255,130,15,255,130
2120 DATA 15,255,130,15,255,130,15,255,130,15,255,130,15,255,130,15
2130 DATA 255,130,15,255,130,15,255,130,15,255,130,15,255,130,15,255
2140 DATA 130,15,255,130,15,255,130,15,255,128,15,255,255,255,255,0
2150 :: REM SPRITE_11 / SINGLECOLOR / COLOR: 0
2160 DATA 255,255,255,128,8,0,128,8,32,128,136,32,128,136,32,128
2170 DATA 136,32,128,136,32,128,136,32,128,136,32,128,136,32,128,136
2180 DATA 32,255,136,32,255,136,32,128,136,32,128,136,32,128,136,32
2190 DATA 128,136,32,128,136,32,128,136,32,128,8,0,255,255,255,0
2200 :: REM SPRITE_12 / SINGLECOLOR / COLOR: 0
2210 DATA 255,255,255,130,8,3,130,8,33,130,8,33,130,8,32,130
2220 DATA 8,32,130,8,32,130,8,32,130,8,33,130,8,3,130,8
2230 DATA 35,130,8,33,130,8,33,130,8,32,130,8,32,130,8,32
2240 DATA 130,8,32,130,8,32,130,8,32,128,8,32,255,255,255,0
2250 :: REM SPRITE_13 / SINGLECOLOR / COLOR: 0
2260 DATA 255,255,255,134,24,0,132,8,32,128,8,32,128,8,32,128
2270 DATA 8,32,129,8,32,129,8,32,129,8,32,129,8,0,129,8
2280 DATA 0,129,8,32,129,8,32,129,8,32,129,8,32,129,8,32
2290 DATA 129,8,32,129,8,32,129,8,32,129,8,32,255,255,255,0
2300 :: REM SPRITE_14 / SINGLECOLOR / COLOR: 0
2310 DATA 255,255,255,131,255,255,131,255,255,131,255,255,131,255,255,131
2320 DATA 255,255,131,255,255,131,255,255,131,255,255,131,255,255,131,255
2330 DATA 255,131,255,255,131,255,255,128,63,255,128,63,255,128,63,255
2340 DATA 128,63,255,128,63,255,128,63,255,128,63,255,255,255,255,0

Fazit

RND ist kein umfangreicher Befehl, hat aber seine Eigenheiten. Die wichtigsten Punkte:

  • RND liefert immer eine Zahl von 0 bis knapp unter 1. Mit INT(RND(1)*N)+A bekommt man ganze Zahlen von A bis A+N-1.
  • Negative Werte setzen einen festen Startwert. Das ist praktisch, wenn man eine reproduzierbare Folge braucht.
  • RND(0) eignet sich gut als Startwert, aber nicht für große Zahlenbereiche: Oberhalb von 256 wird der Höchstwert nicht mehr erreicht.
  • Am sichersten: einmal am Programmanfang X=RND(-TI) oder X=RND(0), danach nur noch RND(1).

Einsatzmöglichkeiten gibt es viele, vom Würfelspiel bis zur Charaktererstellung in einem Rollenspiel, wo Stärke, Geschicklichkeit oder Intelligenz ausgewürfelt werden. Viel Spaß beim Ausprobieren!

Das Video zu diesem Artikel findet ihr auf meinem Youtube-Kanal: