Zufallszahlen in Assembler

Für diesen Beitrag wurde das CBM prg Studio verwendet.
weitersagen ...
Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedIn

CBM prg StudioZufallszahlen in Assembler erzeugen

Früher oder später kommt jeder Assemblerprogrammierer in die Verlegenheit zufällige Werte erzeugen zu müssen. Sei es um ein Puzzel ‚durcheinander zu würfeln‚, den Startpunkt für die nächste Gegnerwelle zu bestimmen oder um ein komplettes Labyrinth (z. B. einen Dungeon für ein Rollenspiel) zu erstellen.

Da ein Computer rein logischen Abläufen folgt, sind zufällige Werte relativ dünn gesäht. Genaugenommen gibt es nichts wirklich zufälliges, es sei denn ein Bauteil schmorrt mal durch 😉 .

  

Wie kommen wir nun an Zufallszahlen?

Warum das Rad neu erfinden?
Vom BASIC kennt ihr bestimmt den RND()-Befehl. Also, warum sich den Kopf zerbrechen und etwas Neues erfinden, warum nicht einfach den BASIC-Befehl nutzen? Zunächst mal haben wir uns ja für Assembler entschieden, da das BASIC nicht unbedingt schnell ist. Dies trifft auch auf die Zufallszahlen zu. Außerdem muss das ROM dazu sichtbar sein, was evtl. nicht immer gewünscht ist. Wer es dennoch nutzen möchte, der findet den Einsprung zur Random-Funktion im ROM an der Adresse $E097.
Um gleich die Funktionsweise der ROM-Routine besser zu verstehen, werfen wir mal einen Blick auf den BASIC-Befehl. Unter BASIC gebt ihr bekanntlich etwas wie RND(175) ein und erhaltet dann eine Zufallszahl im Fließkommaformat, zwischen 0 und 1, wobei weder die 0 noch die 1 jemals als Ergebnis auftauchen werden. Für uns ist als Erstes der Wert in den Klammern von Interesse. Dieser wird verwendet, um den Ursprung für unsere Zufallszahlen zu ‘sähen‚ (im engl. lest ihr häufig etwas von ‘seed‚ hier nur mit zwei e 😉 ). Steht in den Klammern eine Zahl > 0, dann wird als ‘seed‚ der Wert genommen, den ihr auf der Zero-Page an den Adressen  $8B - $8F  findet, es handelt sich hier um eine fünf BYTE lange Fließkommazahl. Nach dem Einschalten wird diese immer auf den selben Startwert ( $80, $4F, $C7, $52, $58 ) gesetzt.  Somit bekommt ihr mit  RND(<positive Zahl>) auch immer die selben Zufallszahlen, nach dem Einschalten oder einem RESET. Außerdem ist nur das Vorzeichen (+PLUS) wichtig und nicht die Zahl. Steht in den Klammern eine Null, dann wird der TIMER verwendet um den Ursprung festzulegen, was relativ zufällig ist. Gebt ihr eine negative Zahl in den Klammern an, dann wird diese durch verwürfeln als Ursprung benutzt. Das ist für Testzwecke hilfreich, da ihr so immer den gleichen Ursprung bekommt, unabhängig davon, was vorher geschehen ist und so immer die selben Zufallszahlen erhaltet.  RND(0) ist zwar eigentlich die empfohlene Methode, aber auch die kann auf dem C64 problematisch sein, RND(-RND(0)) scheint bessere Ergebnisse zu liefern (s. Compute‘s – Mapping the Commodore 64, Seite 26).
Schauen wir jetzt weiter, wie das unter Assembler genutzt werden kann. Wie erwähnt müssen wir nach  $E097 springen, um den RND()-Befehl aufzurufen. Der Wert aus den Klammern muss im Akku und ggf. im FAC1 (Floting Point ACcumulator #1) stehen. Haben wir im Akku eine Null ($00), dann wird, wie eben erwähnt, der TIMER (Timer A und die Echtzeituhr aus dem CIA-1) als Ursprung genommen. Bei einer positiven Zahl ($01 – $7F), wird wieder der Wert aus $8B - $8F als Ursprung verwendet. Ist die Zahl im Akku aber negativ ($80 – $FF), dann wird der Ursprung aus dem FAC1 genommen, den wir natürlich vorher füllen müssen (der FAC1 liegt auf der Zero-Page an den Adressen $61 - $66). Hier im FAC1 finden wir dann auch das Ergebnis des RND()-Befehls. Na toll, was sollen wir jetzt mit einer Fließkommazahl? Uns würde in Assembler ein BYTE viel besser passen. Es hat sich rausgestellt, dass in FAC1 in den BYTES an den Adressen $63 & $64 passende Werte auftauchen. Kontrollieren könnt ihr das mit dem Beispielprogramm am Ende der Seite. Wie bereits anfangs erwähnt, ist diese Funktion nicht besonders schnell. Wie ihr evtl. durch das setzen des Ursprungs bemerkt habt, liefert  RND() ‚pseudo‚ Zufallszahlen. Es wird ausgehend vom gewählten Ursprung eine Reihe von Zufallszahlen erzeugt, die aber bei gleichem Ursprung auch immer gleich (und somit berechenbar) sind. Sie wirken auf uns nur zufällig.

 

Bitte kein BASIC
OK, machen wir uns Gedanken, wie wir einfacher und schneller an Zufallszahlen kommen können.

Die Rasterzeile
Einsteiger kommen hin und wieder auf die Idee, die aktuelle Rasterzeile zu nehmen. Wie ihr bestimmt wisst, baut der C64 sein Bild von oben nach unten auf. Dazu ‚läuft‚ der sog. Rasterstrahl über den Bildschirm. Er wandert auf PAL-Systemen 50 mal und bei NTSC 60 mal in der Sekunde über den kompletten Bildschirm. Wir können nun die aktuelle Position des Rasterstrahls abfragen und so an eine vermeintliche Zufallszahl kommen. Allerdings müssen wir hier sehr aufpassen. Wenn wir z. B. in einer Routine immer erst auf eine bestimmte Rasterzeile warten und erst danach unsere Zufallszahl ermitteln, wird die schon nicht mehr so zufällig sein. Aber man könnte zu Beginn die Rasterzeile nutzten um einen eigenen Startwert zu erzeugen. Wie ihr euch auch immer entscheidet, ihr könnt mit einem  lda $D012  die aktuelle Rasterzeile auslesen und direkt als Zufallszahl verwenden, wirklich ratsam ist das aber nicht! Also, vergesst das ganz schnell wieder!!

 

Der SID macht nicht nur Musik
Der SID-Chip schmeichelt unseren Ohren nicht nur mit dem unvergesslichen C64-Sound, er taugt auch zur Erzeugung von Zufallszahlen (s. Compute! Ausgabe 72 vom Mai 1986). Dazu müsst ihr nur einige Register setzen. Stellt die dritte Stimme des SID auf Rauschen (noise) und dann müsst ihr noch die Frequenz auf einen Wert <> 0 setzen. Ab jetzt findet ihr an $D41B Zufallszahlen im BYTE-Format. Keine Angst, wir aktivieren die Ausgabe nicht, man wird also keinen unerwünschten Ton vernehmen. Diese Einstellungen müsst ihr übrigens nur einmal vornehmen. Aber auch hier bekommen wir evtl. Probleme, was ist, wenn wir den SID und besonders die 3. Stimme für unsere Musik brauchen?

Ab jetzt bekommt ihr mit lda $D41B  eure Zufallszahlen.

 

Der Timer
Der TIMER des C64 wird evtl. am häufigsten für die Ermittlung von Zufallszahlen benutzt. Wenn wir den Timer verwenden, dann sollten wir mehrere TIMER Register miteinander verknüpfen und nicht nur ein einzelnes BYTE betrachten. Die folgende Lösung liefert sehr gute Ergebnisse und wird auch von mir verwendet.

 

Genau diese Code-Sequenz findet ihr übrigens auch im Klassiker Boulder Dash, so schlecht kann dieses Vorgehen dann ja nicht sein.

Hier mal mit dem VICE Monitor herausgesucht (nach einem Klick aufs Bild wird es lesbarer).

Die Zufallszahlen-Routine von Boulder Dash.
Die Zufallszahlen-Routine von Boulder Dash.

Evtl. sollten wir noch beachten, dass der TIMER beim Einschalten immer wieder beim selben Wert beginnt und das kann bei den heutzutage häufig genutzen Emulatoren dazu führen, dass immer die selben Zufallszahlen erzeugt werden. Dies passiert, wenn euer Programm direkt mit der Generierung von Zufallszahlen beginnt (z. B. bei einer Demo). Wartet ihr zunächst aber auf eine Eingabe vom User (z. B. das Drücken des Feuerknopfes zum Starten), dann erledigt sich das Problem von selbst, da der Spieler sicherlich nicht jedes mal in der selben Mikrosekunde nach dem Laden den Feuerknopf drückt.

 

Die Mischung machts
Um das eben beschriebene Problem in den Griff zu bekommen, kann man jetzt versuchen über den Rasterstrahl oder den SID einen Quellwert für die Erzeugung von Zufallszahlen zu ermitteln. Beim Programmstart holen wir uns dann ganz zu Anfang diesen Wert und speichern ihn im RAM. Für die Zufallszahlen verwenden wir die Timer-Routine von oben und machen zum Schluß noch ein letztes  eor  mit unserem Startwert.

 

Wollt ihr nur Zufallszahlen aus einem bestimmten Bereich, dann könnt ihr solange Zahlen erzeugen, bis ihr eine gültige Zufallszahl habt. Schauen wir uns mal ein Beispiel an. Wir möchten nur Zahlen von 15 (untere Grenze UG) bis 42 (obere Grenze OG).

Das kann natürlich auch direkt in die getRamdom:-Routine eingebaut werden und / oder ihr macht die UG und OG dynamisch, in dem diese z. B. im X- & Y-Register übergeben werden.

 

Hier habt ihr jetzt das kleine Beispielprogramm, mit dem ihr die verschiedenen Funktionen testen könnt. Das Programm löscht den Bildschirm und füllt diesen dann per Zufallszahl mit inversen Leerzeichen. Ihr könnt dabei z. B. die Probleme bei der Verteilung (besonders beim Rasterstrahl) erkennen. Um ein Gefühl für die Geschwindigkeit zu bekommen, blinkt der Rahmen, umso breiter die ‚Balken‚ sind, desto langsamer läuft das Programm.
Vergleicht jetzt mal alle Routinen und beobachtet, wie der BS gefüllt wird und wie schnell das jeweils geht. Kommentiert dazu in  getRandom:  einfach die gerade gewünschte Routine ein und die anderen aus.
Euch sollte auffallen, dass  rndBASIC  zwar am langsamsten ist, aber den gesamten BS im Laufe der Zeit gut abdeckt und dabei anscheinend sehr zufällig vorgeht.  rndRaster ist zwar sehr flott, aber es wird nach einem erkennbaren Muster gefüllt und es bleiben viele Lücken. Die  rndSID-Routine ist mit Abstand die schnellste, zeigt kein erkennbares Muster und der komplette BS ist sehr zügig gefüllt. Die rndTIMER-Funktion ist etwas langsamer und benötigt mehr Zeit als rndSID, außerdem kann man eine Art von Muster erkennen. Um diese Unzulänglichkeiten jetzt zu kompensieren könnt ihr am Ende von  getRandom:  die beiden Befehle mit  rndSeed:  einkommentieren und die vier Tests erneut durchführen. Nun sollte sich bei allen eine bessere Verteilung der Zufallszahlen zeigen und der gesamte Bildschirm sollte auch schneller gefüllt werden, außer beim natürlich immer noch sehr langsamen rndBASIC. Als letztes könnt ihr noch mit  getRandom_Range:  testen, wie es sich verhält, wenn wir auf eine Zufallszahl innerhalb eines sehr kleinen Bereichs warten, kommentiert dann das and, clc, adc  aus und lasst euch überraschen. Auch hier ist ein gewaltiger Unterschied, ob  rndSeed verwendet wird, oder nicht!

 

Wie ihr seht gibt es einige Möglichkeiten Zufallszahlen in Assembler zuerstellen, welche ihr nutzt hängt ganz von euch und evtl. dem jeweiligen Einsatzgebiet ab.
Braucht ihr größere Zufallszahlen, dann könnt ihr übrigens einfach zwei, drei oder wieviele BYTES ihr auch immer benötigt einzeln ermitteln und abschließend zu einer großen Zahl zusammenfügen.

 

So, ich hoffe das Mysterium ‚Zufallszahlen in Assembler‚ ist nicht mehr ganz so unheimlich und wünsche euch nun viel Spaß beim Umsetzen in euren eigenen Programmen.

 


Schrott!!Naja...Geht so...Ganz gut...SUPER! (11 Bewertungen | Ø 5,00 von 5 | 100,00%)

Loading...


 

<<< zurück |

 

weitersagen ...
Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedIn

2 Gedanken zu „Zufallszahlen in Assembler“

  1. Hi. Ich will möchte Dummheit hier schreiben, aber ich denke unter rndSIDInit: müsste das Low-Byte „$D40E“ sein, oder? An der Funktion scheint es aber aus optischer Sicht nichts zu ändern.

    1. Cool, gut gesehen und vollkommen richtig.

      Das Low-Byte für die Frequenz gehört nach $D40E.
      Hier hat es, wie von dir erwähnt, zwar keinen direkten Einfluß auf die Funktionsweise, das Programm sollte aber schon korrekt sein.

      Danke für den Hinweis, habe es korrigiert.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Protected by WP Anti Spam