Redcode verstehen
KI-Hinweis: Dieser Text entsteht mit Unterstützung von KI-Systemen, wird aber vor der Veröffentlichung redaktionell geprüft und verantwortet.
Im ersten Schritt hast du zwei Warriors gesehen und ungefähr verstanden, was sie tun. Jetzt sehen wir uns an, woraus eine Redcode-Instruktion eigentlich besteht.
Die Sprache hat wenige Befehle. Die Tiefe steckt woanders — und zwar an einer Stelle, die in vielen Einführungen als Fußnote behandelt wird.
Der Aufbau einer Instruktion
Jede Zelle im Core enthält genau eine Instruktion, und die sieht immer gleich aus:
MOV.I $3, @7
│ │ │ │
│ │ │ └─ B-Operand
│ │ └───── A-Operand
│ └────────── Modifier
└────────────── Opcode
Vier Teile, immer alle vorhanden. Fehlt der Modifier im Quelltext, ergänzt ihn der Assembler nach festen Regeln — er ist also nie wirklich weg, nur unsichtbar. Das ist die häufigste Quelle von Überraschungen für Anfänger.
Beide Operanden haben einen Adressierungsmodus (das $ und das @ oben)
und einen Wert. Werte sind immer relativ zur aktuellen Zelle: $3 heißt
„drei Zellen weiter", nicht „Adresse 3".
Die Befehle
Es gibt 16 Opcodes. Nach Zweck gruppiert:
Daten bewegen und rechnen
| Befehl | Bedeutung |
|---|---|
DAT |
Daten. Führt zum Tod des Prozesses, der darauf läuft. |
MOV |
Kopiert A nach B. |
ADD SUB |
Addiert bzw. subtrahiert A auf B. |
MUL DIV MOD |
Multipliziert, dividiert, Rest. |
NOP |
Tut nichts, kostet aber einen Zyklus. |
Springen
| Befehl | Bedeutung |
|---|---|
JMP |
Springt nach A. |
JMZ JMN |
Springt, wenn B null bzw. nicht null ist. |
DJN |
Zieht eins von B ab, springt dann, wenn B nicht null ist. |
SPL |
Startet einen zusätzlichen Prozess bei A. |
Vergleichen
| Befehl | Bedeutung |
|---|---|
SEQ (CMP) |
Überspringt die nächste Instruktion, wenn A und B gleich sind. |
SNE |
Dasselbe, wenn sie ungleich sind. |
SLT |
Dasselbe, wenn A kleiner als B ist. |
Speicher zwischen Runden — LDP und STP greifen auf den P-Space zu, einen
kleinen Speicher, der eine Runde überlebt. Damit kann ein Warrior aus verlorenen
Runden lernen. Das ist ein fortgeschrittenes Thema; im Standard-Regelwerk
'94nop, mit dem wir hier arbeiten, sind die beiden abgeschaltet.
Das war die ganze Sprache. Wenn dir das dünn vorkommt: völlig richtig. Die Kunst liegt nicht in der Menge der Befehle.
Die Adressierungsmodi
Ein Operand sagt nicht nur welche Zelle gemeint ist, sondern auch wie man dorthin kommt. Dafür gibt es acht Modi. Die vier wichtigsten zuerst:
| Modus | Name | Bedeutung |
|---|---|---|
# |
unmittelbar | Der Wert selbst, keine Zelle. #5 ist die Zahl 5. |
$ |
direkt | Die Zelle so viele Schritte entfernt. $5 ist fünf Zellen weiter. |
@ |
indirekt über B | Gehe zu $5, lies dort das B-Feld, gehe von dort nochmal so weit. |
* |
indirekt über A | Dasselbe, aber über das A-Feld. |
Die indirekten Modi sind der Grund, warum Core War funktioniert: Ein Warrior
kann einen Zeiger fortschreiben und damit den ganzen Speicher bearbeiten, ohne
selbst zu wachsen. Genau das tut der Dwarf aus Schritt 1 mit @bomb.
Dazu kommen vier Modi, die den Zeiger im Vorbeigehen verändern:
| Modus | Bedeutung |
|---|---|
< |
Erst eins vom B-Feld abziehen, dann indirekt darüber gehen. |
> |
Indirekt über B gehen, danach eins draufzählen. |
{ |
Wie <, aber über das A-Feld. |
} |
Wie >, aber über das A-Feld. |
Diese vier sind extrem praktisch — man bekommt Schleifenzähler geschenkt — und gleichzeitig die größte Fehlerquelle der Sprache. Dazu gleich mehr.
Der Modifier ist wichtiger als der Befehl
Jetzt der Punkt, um den es in diesem Kapitel eigentlich geht.
Eine Instruktion hat zwei Felder, A und B. Der Modifier legt fest, welche Felder überhaupt angefasst werden:
| Modifier | Wirkung |
|---|---|
.A |
Nur A nach A. |
.B |
Nur B nach B. |
.AB |
A der Quelle nach B des Ziels. |
.BA |
B der Quelle nach A des Ziels. |
.F |
Beide Felder, A nach A und B nach B. |
.X |
Beide über Kreuz. |
.I |
Die ganze Instruktion, samt Opcode und Modi. |
Das klingt nach Detailkram. Es ist der Unterschied zwischen einem funktionierenden und einem toten Warrior.
Nehmen wir den Imp aus Schritt 1 und ändern nichts außer dem Modifier. Jeweils 2000 Runden gegen den Dwarf:
| Variante | Siege | Niederlagen | Unentschieden | Punkte |
|---|---|---|---|---|
MOV.I $0, $1 |
0 | 510 | 1490 | 74,5 |
MOV.A $0, $1 |
0 | 2000 | 0 | 0,0 |
MOV.B $0, $1 |
0 | 2000 | 0 | 0,0 |
MOV.F $0, $1 |
0 | 2000 | 0 | 0,0 |
MOV.X $0, $1 |
0 | 2000 | 0 | 0,0 |
MOV.AB $0, $1 |
0 | 2000 | 0 | 0,0 |
MOV.BA $0, $1 |
0 | 2000 | 0 | 0,0 |
Zwei Zeichen geändert, und aus einem Warrior, der drei Viertel seiner Kämpfe übersteht, wird einer, der jede einzelne von 2000 Runden verliert.
Der Grund ist einfach, wenn man ihn einmal gesehen hat. Der Imp lebt davon, sich
selbst zu kopieren — und zwar vollständig, inklusive des MOV-Opcodes. Nur
.I kopiert den Opcode mit. Bei jedem anderen Modifier wandert lediglich eine
Zahl in die Nachbarzelle. Der Opcode dort bleibt, was er vorher war: DAT. Der
Imp schreibt sich also sein eigenes Grab und läuft im nächsten Zyklus hinein.
Merk dir: MOV.I und MOV.A sind nicht zwei Varianten desselben
Befehls. Es sind zwei verschiedene Werkzeuge, die zufällig gleich heißen.
In den Simulatoren spiegelt sich das übrigens in der Bauweise wider: Opcode und Modifier werden intern zu einem Wert zusammengefasst, weil sie zusammen erst eine Instruktion ergeben. Wer nur den Opcode liest, hat die Instruktion nicht gelesen.
Die Falle mit < und >
Noch eine Eigenheit, die mehr Warrior-Fehler verursacht als alles andere.
Die Modi < > { } verändern einen Zeiger. Das tun sie auch dann, wenn
der Operand für die eigentliche Rechnung gar nicht gebraucht wird.
JMZ.B $2, <5
JMZ springt, wenn das B-Feld null ist. Ob gesprungen wird oder nicht — das
< hat in jedem Fall bereits eins vom B-Feld der Zelle $5 abgezogen. Der
Seiteneffekt tritt beim Auflösen der Adresse ein, nicht beim Ausführen des
Befehls.
Dazu kommt eine Reihenfolge, die man kennen muss: Der A-Operand wird zuerst aufgelöst, der B-Operand danach. Wenn also beide auf dieselbe Zelle zeigen und beide sie verändern, sieht der B-Operand die Änderung des A-Operanden — aber nicht umgekehrt.
Das ist keine Schikane, sondern ein Werkzeug: Erfahrene Autoren bauen damit
Zähler, die ohne eigene Instruktion auskommen. Für den Anfang gilt vor allem:
Wenn ein Warrior sich unerklärlich verhält, sieh zuerst nach, ob irgendwo
ein < oder > steht, an das du nicht gedacht hast.
Ein Beispiel zum Nachlesen
Mit dem Wissen lässt sich der Dwarf jetzt Zeile für Zeile lesen:
step EQU 4
start ADD.AB #step, $bomb
MOV.I $bomb, @bomb
JMP.A $start
bomb DAT.F #0, #0
ADD.AB #step, $bomb—#stepist die Zahl 4 selbst..ABheißt: A der Quelle auf B des Ziels. Es zählt also 4 auf das B-Feld vonbomb.MOV.I $bomb, @bomb—.Ikopiert die ganze Instruktion. Ziel ist@bomb, also indirekt über das B-Feld vonbomb— genau der Zeiger, den die Zeile davor hochgezählt hat.JMP.A $start— zurück zum Anfang.bomb DAT.F #0, #0— die Bombe. Sie ist gleichzeitig Munition und Zeigerspeicher: Das B-Feld dient als Adresszähler, während der Rest der Zelle als tödliche Ladung durch den Core geschickt wird.
Diese Doppelnutzung — eine Zelle ist Daten und Zeiger und Waffe — ist typisch für Redcode. Speicher ist knapp, und jede Zelle, die man spart, ist eine Zelle weniger, die der Gegner treffen kann.
Was du jetzt weißt
- Eine Instruktion besteht aus Opcode, Modifier und zwei Operanden mit je einem Adressierungsmodus.
- Es gibt 16 Befehle; die Sprache ist absichtlich klein.
#ist ein Wert,$eine Zelle,@und*sind Zeiger.<>{}verändern Zeiger — auch wenn der Operand ungenutzt bleibt.- Der Modifier entscheidet über die Semantik.
MOV.IgegenMOV.Aist der Unterschied zwischen 74,5 und 0,0 Punkten.
Im nächsten Schritt sehen wir uns an, welche Strategien sich aus diesen wenigen Befehlen entwickelt haben — und warum sie einander wie bei Schere, Stein, Papier schlagen.
Alle Zahlen auf dieser Seite wurden mit pMARS unter dem Standard-Regelwerk '94nop über je 2000 Runden nachgerechnet.