Wstęp
Reguły
|
Wojny Rdzeniowe
Reguły gry według standardu CWS'88
(wersja ASCII: cws88.txt)
Spis treści.
1. Wprowadzenie.
2. Reguły walki.
3. Rdzeń.
4. Tryby adesowania.
5. Rozkazy Redcode.
6. Wykonywanie rozkazów.
7. Pseudoinstrukcje.
8. Składnia kodu źródłowego.
1. Wprowadzenie.
Redcode jest językiem asemlerowym, w którym pisane są programy walczące w Wojnach
Rdzeniowych. Redcode jest asemblerem wirtualnego procesora o nazwie MARS. Ponieważ
MARS jako urządzenie nie istnieje, jego działanie musi być symulowane programowo
(stąd też jego nazwa: Memory Array Redcode Simulator), tak więc mianem tym określa
się również programy, które potrafią pobierać z wirtualnej pamięci rozkazy Redcode,
dekodować je i wykonywać, tak jak robiłby to prawdziwy MARS.
2. Reguły walki.
Wojny Rdzeniowe polegają na walce programów napisanych w Redcode. Walka toczy się
w pamięci MARS-a, zwanej rdzeniem. Przed rozpoczęciem bitwy MARS umieszcza
walczących wojowników na rdzeniu dbając o to, aby ich kody nie pokrywały się.
W trakcie rozgrywek MARS w każdym cyklu wykonuje po jednej instrukcji każdego
wojownika, zawsze w tej samej kolejności. Walka kończy się, gdy jeden z walczących
programów zostanie pokonany, lub gdy zostanie wykonana określona liczba cykli.
W drugim przypadku walka pozostaje nierozstrzygnięta i MARS ogłasza remis.
MARS jest systemem wieloprocesowym. Każdy wojownik w momencie uruchomienia
dysponuje jednym procesem, ale w trakcie walki może się podzielić na wiele
procesów. Procesy jednego wojownika tworzą kolejkę i wykonywane są na przemian po
jednej instrukcji. Jeśli któryś z procesów zostanie zniszczony, zostanie również
usunięty z kolejki. Wojownik przegrywa walkę, gdy z jego kolejki procesów zostanie
usunięty ostatni proces.
3. Rdzeń.
Rdzeń jest pamięcią, w której są umieszczane i wykonywane walczące programy.
Pamięć ta jest zorganizowana w zamknięty pierścień komórek. Pojedyncza komórka
składa się z pola operacji, pola argumentu A oraz pola argumentu B. Każda
instrukcja Redcode mieści się dokładnie w jednej komórce pamięci.
Ponieważ rdzeń nie ma ani początku, ani końca, niemożliwe jest przypisanie
poszczególnym komórkom jednoznacznych adresów. Wszystkie adresy są więc liczone
względem bieżącej pozycji. Na przykład adres 0 wskazuje bieżącą instrukcję,
adres -1 instrukcję poprzednią, a adres 1 instrukcję następną.
Cykliczna struktura rdzenia powoduje również, że każda komórka może mieć wiele
różnych adresów. Jeśli przyjmiemy rozmiar rdzenia za M, to bieżącą instrukcję
znajdziemy pod adresami {..., -2M, -M, 0, M, 2M, ...}, poprzednią pod
{..., -1-2M, -1-M, -1, M-1, 2M-1, ...}, a następną pod
{..., 1-2M, 1-M, 1, M+1, 2M+1, ...}.
Przed walką rdzeń jest inicjowany przez wpisanie instrukcji DAT 0, 0 do każdej
jego komórki.
Pola argumentów mogą zawierać liczby całkowite z zakresu [0..M-1].
4. Tryby adresowania.
Redcode dopuszcza stosowanie czterech trybów adresowania. Tryb adresowania
poszczególnych argumentów określa się poprzez odpowiednie oznaczenie ich typów.
Dopuszczalnymi trybami adresowania są:
Tryb natychmiastowy, oznaczany symbolem #. Jego wartość znajduje się w bieżącej
instrukcji. Jeżeli argument A jest natychmiastowy, jego wartości należy szukać
w polu argumentu A, zaś jeśli argument B jest natychmiastowy, wartość znajduje
się w polu argumentu B.
Tryb bezpośredni, przyjmowany domyślnie, gdy nie ma żadnego symbolu określającego
typ argumentu. W trybie bezpośrednim pole argumentu jest adresem, który wskazuje
komórkę będącą wartością argumentu. Adres jest liczony względem aktualnie
wykonywanej instrukcji. Niektóre MARS-y zezwalają na oznaczanie argumentów
bezpośrednich symbolem $.
Tryb pośredni, oznaczany symbolem @. Tutaj, podobnie jak w trybie bezpośrednim,
pole argumentu jest adresem wskazującym jakąś komórkę, jednak jest ona nie
wartością argumentu, lecz wskaźnikiem do niej. Adres komórki będącej wartością
argumentu znajduje się w polu argumentu B wskaźnika i jest on liczony względem
pozycji wskaźnika. Na przykład:
x1 DAT #0, #0 ;komórka docelowa
x2 DAT #0, #-1 ;wskaźnik (adres pośredni)
x3 MOV 0, @-1 ;kopiuje siebie pod adres x1
Tryb pośredni zmniejszany, oznaczany symbolem <. Wyznaczany jest niemal
dokładnie w tak samo, jak tryb pośredni, jedynie wskaźnik (ściślej: wartość jego
pola argumentu B) przed użyciem jest zmniejszany o jeden. Na przykład:
x1 DAT #0, #0 ;komórka docelowa
x2 DAT #0, #0 ;wskaźnik (adres pośredni)
x3 MOV 0, <-1 ;kopiuje siebie pod adres x1
5. Rozkazy Redcode.
DAT A, B
Rozkaz DAT (data) ma dwa zastosowania. Po pierwsze, umożliwia
przechowywanie w swoich polach argumentów różnego rodzaju danych: liczników,
adresów, wskaźników itp. Po drugie, proces, który wykona tę instrukcję, zostaje
usunięty z kolejki procesów. Gdy wszystkie procesy wojownika zostaną usunięte
z kolejki, wojownik przegrywa walkę.
W rozkazie DAT dopuszczalne są tylko dwa typy argumentów: natychmiastowy i
pośredni zmniejszany. Efekt zmniejszenia wskaźnika podczas wyznaczania argumentu
pośrednego zmniejszanego występuje również przy rozkazie DAT. Proces jest bowien
usuwany dopiero po wyznaczeniu obu argumentów.
Argument A może być pominięty, w jego miejsce zostanie wstawiony #0.
MOV A, B
Rozkaz MOV (move) kopiuje A do B. Jeśli argument A jest natychmiastowy,
jego wartość jest wstawiana do pola B komórki wskazywanej przez argument B. Jeśli
argument A jest innego typu, cała komórka wskazywana przez argument A jest
kopiowana do komórki wskazywanej przez argument B.
Argument B nie może być natychmiastowy.
ADD A, B
Rozkaz ADD dodaje A do B i umieszcza wynik w B. Jeśli argument A jest
natychmiastowy, jego wartość jest dodawana do pola B komórki wskazywanej przez
argument B. Jeśli argument A jest innego typu, pole A i pole B komórki wskazywanej
przez argument A są dodawane odpowiednio do pola A i pola B komórki wskazywanej
przez argument B.
Argument B nie może być natychmiastowy.
SUB A, B
Rozkaz SUB (subtract) odejmuje A od B i umieszcza wynik w B. Działanie
tego rozkazu jest identyczne, jak rozkazu ADD, jedynie zamiast operacji dodawania
jest odejmowanie.
Argument B nie może być natychmiastowy.
JMP A, B
Rozkaz JMP (jump) skacze do A. Skok jest wykonywany do instrukcji
wskazywanej przez argument A. Argument B może być pominięty, w jego miejsce
zostanie wstawiony #0.
Argument A nie może być natychmiastowy.
JMZ A, B
Rozkaz JMZ (jump if zero) wykonuje skok do A, gdy B jest zerem. Jeśli
argument B jest natychmiastowy, badana jest jego wartość, natomiast jeśli jest
innego typu, badana jest wartość pola B komórki wskazywanej przez argument B.
Ewentualny skok wykonywany jest do instrukcji wskazywanej przez argument A.
Argument A nie może być natychmiastowy.
JMN A, B
Rozkaz JMN (jump if non-zero) wykonuje skok do A, gdy B jest różne od zera.
Działanie tego rozkazu jest identyczne (za wyjątkiem warunku skoku), jak rozkazu
JMZ.
Argument A nie może być natychmiastowy.
DJN A, B
Rozkaz DJN (decrement and jump if non-zero) zmniejsza B i skacze do A,
gdy B po zmniejszeniu jest różne od zera. Jeśli argument B jest natychmiastowy,
zmniejszana (i później badana) jest wartość argumentu B. Jeśli argument B jest
innego typu, zmniejszana (i później badana) jest wartość pola B komórki
wskazywanej przez argument B. Ewentualny skok wykonywany jest do instrukcji
wskazywanej przez argument A.
Argument A nie może być natychmiastowy.
CMP A, B
Rozkaz CMP (compare, skip if equal) porównuje A z B, po czym przeskakuje
następną instrukcję, jeśli A i B są równe. Jeśli argument A jest natychmiastowy,
porównywana jest wartość argumentu A z wartością pola B komórki wskazywanej przez
argument B. Jeśli argument A jest innego typu, porównywane są całe komórki (tzn.
zarówno ich pola operacji, jak i pola argumentów) wskazywane przez argument A i
argument B.
Argument B nie może być natychmiastowy.
SPL A, B
Rozkaz SPL (split) uruchamia pod A nowy proces. Nowopowstały proces
wykonywany jest od instrukcji wskazywanej przez argument A. Bezpośrednio po
wykonaniu rozkazu SPL wykonywana jest jednak ta sama instrukcja, jaka byłaby
wykonana, gdyby ów nowy proces nie powstał (nowy proces dodawany jest bowiem
na końcu kolejki procesów). Jeśli kolejka procesów jest pełna, SPL nie tworzy
nowego procesu.
x1 SPL 0
x2 JMP 0
W powyższym przykładzie kolejność wykonywania poszczególnych procesów jest
następująca:
[proces 1] x1 SPL 0 ;proces 1 tworzy proces 2 pod x1
[przeciwnik] ;wykonywany jest kod przeciwnika
[proces 1] x2 JMP 0 ;proces 1 zapętla się
[przeciwnik]
[proces 2] x1 SPL 0 ;proces 2 tworzy proces 3 pod x1
[przeciwnik]
[proces 1] x2 JMP 0
[przeciwnik]
[proces 2] x2 JMP 0 ;proces 2 zapętla się
[przeciwnik]
[proces 3] x1 SPL 0 ;proces 3 tworzy proces 4 pod x1
[przeciwnik]
[proces 1] x2 JMP 0
[przeciwnik]
[proces 2] x2 JMP 0
[przeciwnik]
[proces 3] x2 JMP 0 ;proces 3 zapętla się
[przeciwnik]
[proces 4] x1 SPL 0 ;proces 4 tworzy proces 5 pod x1
(...) ;itd...
Argument B może być pominięty, w jego miejsce zostanie wstawiony #0, argument
A nie może być natychmiastowy.
SLT A, B
Rozkaz SLT (skip if less than) porównuje A z B, po czym przeskakuje
następną instrukcję, jeśli A jest mniejsze niż B. Działanie rozkazu jest
analogiczne do CMP.
Argument B nie może być natychmiastowy.
Redcode, podobnie jak inne języki asemblerowe, wprowadza pewne ograniczenia
w używaniu argumentów natychmiastowych. Nie wolno ich używać tam, gdzie rozkaz
spodziewa się adresu, np. przy skokach. Niedopuszczalne są zatem poniższe
konstrukcje (? oznacza dowolny typ argumentu):
MOV ?A, #B
ADD ?A, #B
SUB ?A, #B
CMP ?A, #B
SLT ?A, #B
JMP #A, ?B
JMZ #A, ?B
JMN #A, ?B
DJN #A, ?B
SPL #A, ?B
Wyjątkiem jest tu rozkaz DAT, który w obu argumentach dopuszcza wyłącznie
adresowanie natychmiastowe (#) i pośrednie zmniejszane (<).
Niektóre MARS-y umożliwiają obejście tego ograniczenia, przyjmując adres
argumentów natychmiastowych równy 0.
6. Wykonywanie rozkazów.
Ten punkt zawiera opis techniczny przeznaczony dla projektantów MARS-ów oraz
zaawansowanych graczy. Jego zadaniem jest jak najdokładniejsze zdefiniowanie
reguł, jakimi rządzą się Wojny Rdzeniowe oraz rozwianie wszelkich możliwych
niejasności.
MARS, aby poprawnie zinterpretować instrukcję Redcode, korzysta z następujących
wewnętrznych rejestrów:
RR - rejestr rozkazu
RA - rejestr argumentu A
RB - rejestr argumentu B
PC - licznik programu (adres aktualnie wykonywanej instrukcji)
AdrA - adres argumentu A
WskA - wskaźnik argumentu A
AdrB - adres argumentu B
WskB - wskaźnik argumentu B
A - wartość argumentu A
B - wartość argumentu B
Rejestry RR, RA i RB służą do przechowywania całych komórek pamięci, można więc
w nich wyodrębnić trzy pola: Op, A i B (odpowiednio: pole operacji, pole argumentu
A i pole argumentu B). Pozostałe rejestry mogą przechowywać liczby całkowite
z zakresu [0..M-1], gdzie M jest wielkością rdzenia. Wszystkie operacje
arytmetyczne MARS wykonuje modulo M.
Instrukcje wykonywane przez MARS-a posługują się wyłącznie adresami względnymi,
jednak MARS musi mieć możliwość bezwzględnego adresowania rdzenia. Rejestr PC
przechowuje zatem bezwzględny adres wykonywanej instrukcji. Rejestry AdrA, WskA,
AdrB i WskB przechowują bezwzględne adresy argumentów lub wskaźników liczone
względem adresu umieszczonego w PC.
Pierwszym krokiem MARS-a jest odczytanie instrukcji do wykonania
i zapamiętanie jej w rejestrze RR. Instrukcja ta znajduje się na rdzeniu
pod bezwzględnym adresem przechowywanym w rejestrze PC:
RR = Rdzeń[PC]
Kolejnym krokiem jest opracowanie argumentów. Tak wyznaczany jest natychmiastowy argumentu A:
AdrA = PC
RA = Rdzeń[AdrA]
A = RA.A
Bezpośredni argument A:
AdrA = (PC + RR.A) mod M
RA = Rdzeń[AdrA]
A = RA.B
Pośredni argument A:
WskA = (PC + RR.A) mod M
RA = Rdzeń[WskA]
AdrA = (WskA + RA.B) mod M
RA = Rdzeń[AdrA]
A = RA.B
Pośredni zmniejszany argument A:
WskA = (PC + RR.A) mod M
RA = Rdzeń[WskA]
Rdzeń[WskA].B = (Rdzeń[WskA].B + M - 1) mod M
AdrA = (WskA + RA.B) mod M
AdrA = (WskA + M - 1) mod M
RA = Rdzeń[AdrA]
A = RA.B
Argument B wyznaczany jest podobnie, jedynie zamiast AdrA, WskA i
RR.A używane są odpowiednio AdrB, WskB i RR.B, a zamiast wartości A wyliczana
jest wartość B:
B = RB.B
Po wyznaczeniu obu argumentów MARS może przystąpić do wykonywania
instrukcji umieszczonej w RR.Op. Działanie wszystkich rozkazów zostało opisane
w punkcie 5, należy jedynie pamiętać o następujących zasadach:
- MOV albo wpisuje A do Rdzeń[AdrB].B, albo RA do Rdzeń[AdrB],
- ADD i SUB z natychmiastowym argumentem A operują na A i B, wynik umieszczają w Rdzeń[AdrB].B,
- ADD i SUB z adresowym argumentem A operują na RA.A i RB.A oraz na RA.B
i RB.B, wyniki umieszczają odpowiednio w Rdzeń[AdrB].A i Rdzeń[AdrB].B,
- skoki i SPL operują na rejestrze AdrA,
- skoki warunkowe testują rejestr B,
- DJN zmniejsza Rdzeń[AdrB].B,
- CMP operuje albo na rejestrach A i B, albo na RA i RB,
- SLT zawsze operuje na rejestrach A i B,
- jeśli rozkaz nie wykonał skoku, to PC = (PC + 1) mod M
7. Pseudoinstrukcje.
Obok jedenastu rozkazów Redcode dopuszcza stosowanie dwóch pseudoinstrukcji: END
i EQU. Pseudoinstrukcje nie są rozkazami Redcode, a jedynie dyrektywami
kompilatora.
END <etykieta>
Pseudoinstrukcja END służy do oznaczania punktu startowego oraz końca programu.
Punktem startowym programu staje się instrukcja wskazana etykietą podaną jako
argument. Argument jest opcjonalny i można go opuścić, punktem startowym będzie
wówczas pierwsza instrukcja w programie. Wszystko, co w kodzie źródłowym znajduje
się po pseudoinstrukcji END, jest przez kompilator ignorowane.
<etykieta> EQU <A>
Pseudoinstrukcja EQU (equate) powoduje podmienianie każdego wystąpienia
etykiety <etykieta> napisem <A>. Na przykład:
x1 DAT #0, #x1
DAT #0, #x1
DAT #0, #x1
znaczy to samo, co
x1 DAT #0, #0
DAT #0, #-1
DAT #0, #-2
ale
x1 EQU 0
DAT #0, #x1
DAT #0, #x1
DAT #0, #x1
znaczy tyle, co
DAT #0, #0
DAT #0, #0
DAT #0, #0
8. Składnia kodu źródłowego.
Program napisany w Redcode składa się z wierszy o następującym formacie:
<etykieta> <instrukcja> <komentarz>
Elementy te są opcjonalne, zatem pojedynczy wiersz nie musi zawierać ich
wszystkich. Dopuszczalne też są puste wiersze. Znakami rozdzielającymi są spacja
i znak tabulacji. Etykieta jest ciągiem liter i cyfr zaczynającym się literą.
Małe i duże litery są nierozróżnialne, do liter zalicza się również kreska
podkreślenia. Etykieta może mieć dowolną długość, ale tylko pierwsze osiem znaków
jest znaczących. Komentarz jest ciągiem dowolnych znaków zaczynającym się
średnikiem i kończącym się wraz z końcem wiersza.
Instrukcja składa się z rozkazu i dwóch argumentów, przy czym każdy z argumentów
może być poprzedzony symbolem określającym jego typ:
<rozkaz> <typ A><argument A>, <tyb B><argument B>
Dopuszczalnymi rozkazami są DAT, MOV, ADD, SUB, JMP, JMZ, JMN, DJN, CMP, SPL, SLT,
END oraz EQU. Dopuszczalnymi typami argumentów są: # (adresowanie natychmiastowe),
@ (adresowanie pośrednie) i < (adresowanie pośrednie zmniejszane). Oznaczenie
typu argumentu można pominąć, spowoduje to przyjęcie typu bezpośredniego.
Argumenty są wyrażeniami arytmetycznymi składającymi się z liczb całkowitych,
etykiet oraz operatorów dodawania (+), odejmowania (-), mnożenia (*) oraz
dzielenia całkowitego (/). Dopuszczalne jest stosowanie nawiasów w wyrażeniach.
CWS'88 w oryginale nie zezwala na oddzielanie argumentów przecinkami, podobnie
jak nie zezwala na oddzielanie spacjami składników wyrażeń arytmetycznych.
Obecnie jednak większość MARS-ów omija te ograniczenia.
---------------------------------------------------------------------------------------------------------------
Opracował Adam Ryba na podstawie:
[1] International Core War Society, "Core Wars Standard of 1988", 1988.
[2] Mark A. Durham, "Introduction to Redcode", 1991.
[3] Mark A. Durham, "EMI'88, Execute MARS Instruction a'la ICWS'88", 1991.
Wrocław, 1995.03.05
|