Redcode kezdőknek

1.10-es magyar verzió, írta Ilmari Karonen, fordította Bíró Csaba


Tartalom


Előszó

Napjainkban nem túl sok kezdő érdeklődik a Core War nevű játék iránt. Ez tulajdonképpen természetes - nem olyan sok ember találja assembly kódok optimalizálását szórakoztató feladatnak - viszont egy másik ok, hogy a kezdet fokozottan nehéz, ugyanis igen nehezen található információ a játék alapjairól. Igaz, van sok jó anyag, de a legtöbbjük vagy túl technikai, vagy elavult, vagy nehezen elérhető, vagy egyszerűen nem teljes.

Hát ezek az okok vezettek ennek az anyagnak a megírásához. A célom, hogy elvezessem a kezdőket a Core War-ral és Redcode-dal való első találkozásuktól addig a pontig, amikor már önállóan is tudnak működő (ha nem sikeres) harcost írni, és megérthetnek technikaibb jellegű cikkeket is.

Őszintén bevallom, hogy én magam is kezdő vagyok a játékhoz. A nyelvet ugyan elég jól ismerem, de még nem írtam igazán sikeres harcost. Mégis úgy döntöttem, nem várok addig, amíg tapaszaltabb leszek, hanem azonnal megírom ezt az ismertetőt, amíg frissen élnek bennem az emlékek, hogy milyen kezdőnek lenni, és próbálkozni a játék megértésével.

Ez az ismertető abszolút kezdőknek lett szánva. Semmi előismeret nem szükséges az assembly nyelről (meg úgy általában a progamozásról), bár az alapdolgok ismerete elősegítheti a megértést. A Redcode, főleg a modern változatok, lehet, hogy assemly kód jellegűnek tűnik, de annál sokkal absztraktabb, és részleteiben teljesen más, mint bármelyik assembly nyelv.

A Redcode azon fajtája, amit ebben az anyagban (leginkább) használunk, a jelenlegi de facto, és ez az ICWS '94 Standard Draft a pMARS 0.8 kiterjesztéseivel. (Valahogy úgy, ahogy a HTML a Netscape kiterjesztéseivel... hmm... szerencsére még nincs Microsoft Corewar Szimulátor. Lehet, hogy túl kicsinek találják a piacot.) A korábbi '88-as standard-et csak említés szintjén tárgyaljuk, a fő célunk a '94-es standard bemutatása. Azok, akik mégis azt akarják tanulni, könnyen találhatnak egy csomó leírást róla a weben.

Fontos:: A Redcode - vagy bármely más programozási nyelv - tanulására nincs jobb módszer, mint a szigorú, lépésről-lépésre való haladás. Ráadásul én megpróbáltam jól tanulható anyagot összeállítani; de ha valamit át akarsz ugrani, hát tedd. Erre való a Tartalomjegyzék című fejezet.

Meg kell még jegyeznem, hogy gyakran arra fogok kényszerülni, hogy valamit hamarabb mutassak meg, mint hogy elmagyaráznám a jelentését. Ezért ha valami nem tűnik azonnal érthetőnek, olvass egy kicsit tovább, és ha még mindig nem érted, próbálj meg nézelődni a szövegben, hogy hol van elmagyarázva.

Persze mindenki másképp tanul, szóval a saját módszered lehet, hogy jobban beválik, mint amit én kitaláltam. De ha valamit unalmasnak találván teljesen átugrasz, megvan az esélyed arra, hogy kihagysz valami fontosat. Megpróbáltam a fontos részeket kiemelve írni, hogy tudd, hol kell megállni és gondolkozni. Ennek ellenére próbálj meg mindent figyelmesen olvasni. Mindent nem részletezhettem ki, mert akkor ez a szöveg olvashatatlanul hosszúra nyúlt volna.


A Core War bemutatkozik

Mi a Core War?

A Core War (vagy Core Wars) programozási játék, ahol assembly programok próbálják megsemmisíteni egymást egy képzeletbeli számítógép memóriájában. A programok (vagy harcosok) egy speciális nyelven íródnak, aminek neve Redcode, őket pedig egy MARS nevű program futtatja (MARS=Memory Array Redcode Simulator).

Mind a Redcode, mind a MARS környezet lényegesen le van egyszerűsítve a valódi számítógéprendszerekhez képest. Ez jó dolog, tekintve a CW programozás célját. Ha a játék egy valódi assembly nyelvet használna, lenne két vagy három ember a Földön, aki képes volna effektív és életképes programot írni, és még lehet, hogy ők sem értenék programjaik működését tökéletesen. Természetesen ez is nagy kihívás lenne, tele lehetőségekkel, de valószínűleg az is évekig tartana, hogy az ember egy közepes erősségű játékossá váljon.

Hogyan működik?

A rendszer, amiben a programok futnak, fölöttébb egyszerű. A core (a virtuális gép memóriája), egy utasításokat tartalmazó lista, ami indításkor, a benne levő harcra kész programoktól eltekintve üres. A core körbefordul, tehát az utolsó utasítást ismét az első követi.

Persze a programok nem tudhatják, hogy a core hol végződik, hiszen nincs abszolút memóriacím. Szóval a 0-ás címen nem az első utasítást van, hanem az amelyik a címzést tartalmazza. A következő utasítás az 1-es és az előző pedig nyilván a -1-es című.

Amint az látható, a memória legkisebb egysége a Core War-ban az utasítás, ellentétben az általános gyakorlatban megszokott byte-tal. Minden Redcode utasítás 3 részből áll maga az utasítás, a forrás cím (ennek neve A-mező), és a cél cím (ez a B-mező). És bár lehet például adatot mozgatni az A- és a B-mező között, de az utasításokat általában, mint összefüggő blokk kell felfogni.

A programok végrehajtása ugyanilyen egyszerű. A MARS egyszerre egy utasítást hajt végre, azután ugrik a következőre a memóriában, hacsak a vérehajtott utasítás nem küldi egy másik címre. Ha (mint általában) egyszerre több program is fut, akkor a programok egyszerre hajtódnak végre: egy utasítás az egyikből, aztán egy a másikból, és így tovább. Így minden utasítás végrehajtása ugyanannyi időt vesz igénybe: egy ütemet, legyen az utasítás MOV, DIV vagy akár DAT (ami mellesleg leállitja a processt)...


Első lépések a Redcode-dal

A Redcode utasításkészlete

A Redcode utasítások száma minden új standard bevezetésekor növekedett, így lett az eredeti 5 utasításból jelenleg 18 vagy 19. És ebben még nincsenek benne az új módosítók és címzési módok, amikből kombinációk százai állíthatók elő. Szerencsére nem kell minden kombinációt megtanulnunk. Elég ha emlékszünk az utasításokra, és hogy a módosítók mit változtatnak rajtuk.

Íme egy lista a Redcode-ban használt utasításokról:

Ne aggasszon, ha némelyik enyhén szólva furcsának tűnik. Amint mondtam, a Redcode, absztrakt természetéből adódóan, egy kicsit különbözik az általánosan használt assembly nyelvektől.

Az Imp

Az az igazság, hogy a Redcode legegyszerűbb elemei a legfontosabbak. A legtöbb harcos-tipust még az új utasítások bevezetése előtt kitalálták. Talán a legelső Core War program az Imp volt, ezt A. K. Dewdney publikálta 1984-ben az Scientific American-ben. Ez a cikk volt az első Core War-ról szóló tanulmány a szaksajtóban.

        MOV 0, 1

Ennyi. Egyetlen vacak MOV. De mit is csinál? Hát MOV, természetesen utasítást másol. Ha emlékszel még, a Core War-ban minden cím relatív az aktuális utasításhoz, így az Imp tulajdonképpen önmagát másolja egy utasítással maga elé.

        MOV 0, 1         ; ezt hajtotta végre most
        MOV 0, 1         ; ezt az utasítást hajtja végre mindjárt

Tehát az Imp azt az utasítást fogja vérehajtani, amit ő másolt oda az előbb! És mivel az pontosan ugyanaz mint az első, újra lemásolja magát, megint továbbmegy a másolásra, és így folytatva a végrehajtást telerakja a core-t MOV-val. A core-nak pedig nincs tulajdonképpeni vége, így az Imp miután teljesen teletöltötte, eléri a kiindulási pontját, és boldogan folytatja a futást a körbe-körbe az idők végezetéig.

Tehát az Imp elkészíti a saját kódját, aztán végrehajtja azt! A Core War-ban az önmódosítás inkább szabály, mint kivétel. Hatékonynak, sikeresnek kell lenned, és ez majdnem mindig azzal jár, hogy a programodnak módosítania kell magát futás közben. Szerencsére az absztrakt környezet ezt sokkal inkább lehetővé teszi, mint a szokásos assembly-ben.

Ehhez kapcsolódva elmondanánk, hogy a Core War-ban természetesen nincsen cache, ám ez így mégsem igaz, mert a végrahajtott utasítás cache-elődik. Így nem változtathatunk meg egy utasítást önmaga végrehajtása közben. De erről majd inkább később...

A Dwarf

Az Impnek, mint harcosnak egyetlen hátulütője van. Nem fog túl sok meccset megnyerni, mert amikor valakit felülír, az is elkezdi a MOV 0, 1-eket végrehajtani, ő is Imppé válik, és az eredmény döntetlen. Más programok megöléséhez DAT utasítást kell annak kódjába másolni.

Ez az, amit a Dewdney által írt másik klasszikus harcos, a Dwarf csinál. "Bombázza" a core-t egyenlő távolságonként DAT-okkal, és mellesleg soha nem lövi szét önmagát.

        ADD #4, 3        ; a végrahajtás itt kezdődik
        MOV 2, @2
        JMP -2
        DAT #0, #0

Tulajdonképpen ez nem pontosan ugyanaz, mint amit Dewdney írt, de pontosan ugyanazt teszi. A végrehajtás ismét az első utasításon kezdődik, ami most egy ADD. Az ADD összeadja a forrást és a célt, az eredményt pedig a célra teszi. Ha valaki otthonosan mozog más assembly nyelvekben, már biztos felismerte a # jelet, ami egy módja a közvetlen címzés jelzésének. Vagyis az ADD hozzáadja a 4-es számot (és nem a 4-es címen található számot) a 3-as címen található utasításhoz. Mivel pedig az ADD-tól számított 3. címen található utasítás a DAT, ezért az eredmény a következő lesz:

        ADD #4, 3
        MOV 2, @2        ; következő utasítás
        JMP -2
        DAT #0, #4

Ha két utasítást összeadssz, akkor az A- és a B-mezők egymástól függetlenül összeadódnak. Ha egy egyszerű számot adsz hozzá egy utasításhoz, akkor az alapértelmezésben annak B-mezőjéhez adódik. Az is megengedett, hogy a B-mezőben #-t használjunk. Ekkor az A-mező ugyanannak az ADD-nak a B-mezőjéhez adódik.

A közvetlen címzés egyszerűnek és ismerősnek tűnhet, ám az új módosítók az ICWS '94 standard-ben új trükköket tesznek vele lehetővé. De nézzük előbb a Dwarf-ot.

A MOV megint egy új címzésmódot mutat nekünk: a @-t, vagyis az indirekt címzést. Ez azt jelenti, hogy a DAT nem önmagára másolódik, ahogy az elsőre tűnhet (milyen jó is lenne), hanem arra az utasításra, amire a B-mezője mutat. Íme:

        ADD #4, 3
        MOV 2, @2  ;  --.
        JMP -2     ;    | +2
        DAT #0, #4 ; <--' --. A MOV B-mezője ide mutat.
        ...                 |
        ...                 | +4
        ...                 |
        DAT #0, #4 ; <------' A DAT B-mezője ide mutat.

Amint látható, a DAT négy utasítással maga elé másolódott. A következő utasítás, a JMP, egyszerűen visszaugrik két utasításnyit, vissza az ADD-ra. Mivel a JMP nem veszi figyelembe a B-mezőjét, üresen hagytam neki. A MARS 0 értékkel fogja inicializálni.

Az is látható, hogy a MARS nem kezdi el követni a hosszabb indirekt láncokat. Ha például az indirekt operandus olyan utasításra mutat, aminek mondjuk 4 a B-mezője, akkor az aktuális cél 4 utasítással utána lesz, mindegy mi a címzésmód.

Tehát az ADD és a MOV fog újra végrehajtódni. Mire a végrehajtás újra a JMP-hez ér, a core így fog kinézni:

        ADD #4, 3
        MOV 2, @2
        JMP -2           ; következő utasítás
        DAT #0, #8
        ...
        ...
        ...
        DAT #0, #4
        ...
        ...
        ...
        DAT #0, #8

A Dwarf folytatja a DAT-ok dobálását minden negyedik utasításra, amíg körbe nem ér, és el nem éri önmagát:

        ...
        DAT #0, #-8
        ...
        ...
        ...
        DAT #0, #-4
        ADD #4, 3        ; következő utasítás
        MOV 2, @2
        JMP -2
        DAT #0, #-4
        ...
        ...
        ...
        DAT #0, #4
        ...

Most az ADD visszaállítja a DAT-ot #0, #0-ra, a MOV feleslegesen erőlteti magát azzal, hogy a DAT-ot odamásolja, ahol egyébként is van, aztán a program mindent kezd elölről.

Természetesen ez csak akkor működik, ha a core mérete 4-gyel osztható, mert különben a Dwarf, lelőné a DAT utáni valamelyik utasítást, megölve így önmagát. Szerencsére a leginkább elterjedt core-méret jelenleg 8000, esetleg 8192, 55400, illetve 800 - mind osztható 4-gyel, tehát Dwarfunk biztonságban van.

Itt megjegyzem, hogy a DAT #0, #0 a harcosban nem is lett volna feltétlenül szükséges, ugyanis a core eredetileg DAT 0, 0-val van feltöltve. Én eddig három pontot tettem az üres helyekre (...), és ezt a jelölést fogom ezután is használni, mert rövidebb és jobban átlátható.

A címzési módok

A Core War első verziójában csak a közvetlen (#, a relatív ($ vagy semmi) és a B-mező inirekt (@) címzésmódok léteztek. Később, az előcsökkentő, vagyis a < módot is bevezették. Ez ugyanaz, mint az indirekt mód, csak ennél mielőtt a cél cím kiszámolásra kerülne, a célra mutató szám eggyel csökken.

        DAT #0, #5
        MOV 0, <-1       ; következő utasítás

Mikor ez a kódrészlet végrehajtódik, az eredmény a következő lesz:

        DAT #0, #4 ;  ---.
        MOV 0, <-1 ;     |
        ...        ;     | +4
        ...        ;     |
        MOV 0, <-1 ; <---'

Az ICWS '94 standard-be 4 újabb címzési mód került, ezek leginkább az A-mező indirekciókat kezelik le, így használhatjuk mind a 8 lehetséges módot:

A utónövelő mód hasonló az előcsökkentőhöz, azzal a különbséggel, hogy a célra mutató szám eggyel növekszik, miután az utasítás végrehajtódik (értelemszerűen).

        DAT #5, #-10
        MOV -1, }-1      ; következő utasítás

végrehajtás után a következőképpen néz ki:

        DAT #6, #-10 ;  --.
        MOV -1, }-1  ;    |
        ...          ;    |
        ...          ;    | +5
        ...          ;    |
        DAT #5, #-10 ; <--'

Egy fontos dolgot meg kell jegyezni az előcsökkentéssel és az utónöveléssel kapcsolatban: a mutató szám akkor is csökkentve/növelve lesz, ha a címet nem használjuk semmire. Így például a JMP -1, <100 is csökkenti a 100-as utasítást, pedig azt, hogy az azon a címen lévő érték mire mutat, nem használjuk. A DAT <50, <60 is csökkenti a címeket, miközben megöli a process-t.

A process várakozó sor

Ha figyelmesen nézted az utasítástáblázatot pár fejezettel feljebb, akkor lehet, hogy meglepődtél az SPL nevű utasításon. Ez természetesen nem található meg a szokásos assembly nyelvekben...

Még a Core War korai történetében vetették fel, hogy a multitaskinget a játék részévé kellene tenni, mert úgy sokkal érdekesebb lenne. Mivel az egyszerű időosztásos technika nem illene az absztrakt Core War környezethez (mivel például egy operációs rendszer kellene, hogy kontrollálja), kitaláltak egy rendszert, miszerint minden ciklus alatt egy process hajtódik végre egy adott programból.

Az utasítás, amit új process-ek indítására használhatunk, az SPL. Akárcsak a JMP-nél, itt is egy címet kell megadnunk az A-mezőben. A különbség a JMP és az SPL között, hogy az SPL amellett, hogy az új címre ugrik, folytatja a végrehajtást a következő utasításon is párhuzamosan.

A két - vagy több - így készült process egyenlően osztja a gépidőt egymás közt. Egy szimpla process counter helyett, ami az aktuális utasításra mutatna, a MARS egy process várakozó sort használ, ahol a végrehajtandó processek listája áll, olyan sorrendben, ahogy indítva lettek. Egy SPL-lel létrehozott process pontosan az aktuális után illesztődik be (de abban a körben már nem hajtódik végre - megj. a fordítótól), amik pedig DAT-ot hajtanak végre, kikerülnek a sorból. Ha minden process leáll, a harcos veszít.

Fontos megjegyezni, hogy minden programnak saját process várakozó sora van. Ha több program van a core-ban, azok felváltva hajtódnak végre, egy ciklus alatt mindegyikből egy utasítás, tekintet nélkül a várakozó sor hosszára, és az idő mindig egyenlően osztva adott program processei közt. Ha az 1-es programnak 3 processe van, a 2-esnek pedig csak 1, akkor a végrehajtás sorrendje a következő lesz:

  1. 1-es program, 1-es process
  2. 2-es program, 1-es process
  3. 1-es program, 2-es process
  4. 2-es program, 1-es process
  5. 1-es program, 3-as process
  6. 2-es program, 1-es process
  7. 1-es program, 1-es process
  8. 2-es program, 1-es process

Végül egy rövid példa az SPL használatára. (Többet erről majd a későbbiekben.)

        SPL 0            ; itt kezdődik a végrehajtás
        MOV 0, 1

Minthogy az SPL önmagára mutat, egy ciklus után a process-ek így néznek ki:

        SPL 0            ; itt a második process
        MOV 0, 1         ; itt az első process

Miután mindkét process végrehajtódik, a core így fest:

        SPL 0            ; itt a harmadik process
        MOV 0, 1         ; itt a második process
        MOV 0, 1         ; itt az első process

Tehát ez a kód láthatóan impek sorozatát indítja, egyiket a másik után, és ezt egészen addig csinálja, amig az impek körbe nem érnek a core-on és felül nem írják az SPL-t.

A process várakozó sor mérete minden programnál korlátozva van. Ha a program eléri a maximális process-számot, akkor az SPL csak a következő utasításon folytatja a végrehajtást, tulajdonképpen leutánozva a NOP-ot. A legtöbb esetben a process korlát igen nagy, gyakran a core méretével egyezik meg, de lehet kevesebb is (akár 1; ekkor a multitasking nem engedélyezett).

És lám az igazság gyakran távolabb áll tőlünk, mint a fikció. Nemrégiben egy web oldalon jártam, aminek címe ez volt: "Kitalált utasítások". A tényleg abszurd ötletek közt találtam egy utasítást: "BBW - Branch Both Ways - Ágazz kétfelé". Mivel a többi utasítás tényleg kitaláció volt, megállapítottam, hogy a szerző nem túl járatos a Redcode-ban...

Az utasítás módosítók

A legfontosabb újdonságok, amit az ICWS '94 standard hozott, nem az új utasítások vagy a címzésmódok voltak, hanem a módosítók. A régi '88-as standard-ben a címzésmód elve eldöntötte, hogy az utasítás melyik részére vonatkozik. Pl. a MOV 1, 2 mindig egész utasítást mozgat, míg a MOV #1, 2 csak egy számot. (És mindig a B-mezőre!)

Természetesen ez néha problémát okoz. Mit tegyünk, ha egy utasításnak csak az A- és B-mezőjét akarjuk átmásolni, és nem az egész címet. (Pl. ADD-ot akarsz használni.) És ha valamit a B-mezőről az A-mezőre akarsz másolni? (Lehetséges, de csak igen trükkösen.) A helyzet tisztázása végett vezették be az utasítás mósosítókat.

A mósosítók nem mások mint toldalékok, amiket az utasítások után írunk. Ezek határozzák meg, hogy a forrásnak és a célnak melyik részére hasson az utasítás. Például a MOV.AB 4, 5 a 4-es utasítás A-mezőjét az 5-ös utasítás B-mezőjére mozgatja. Összesen 7 különböző módosító van:

Természetesen ugyanezek a módosítók használhatók minden utasításhoz, nemcsak a MOV-hoz, bár néhány utasítás, mint a JMP vagy az SPL nem veszi figyelembe őket. (Mért is tennék? Nem mozgatnak adatot, csak ugranak!)

Láttuk, hogy nem minden módosító hat minden utasításra, de megpróbálnak a lehető leghasonlóbban viselkedni ahhoz, amint a módosító jelez. A leggyakrabban előforduló példa a .I módosító. A nyelv absztraktsága miatt a numerikus műveletek nem lettek definiálva az utasításokra, így a matematikai műveletek nincsenek hatással rájuk. Ez azt jelenti, hogy a MOV, a SEQ, és az SNE (és persze a CMP, ami ugyanaz, mint a SEQ) kivételével a .I módosító ugyanazt jelenti, mint a .F.

Van egy másik dolog is, amit fontos megjegyezni a .I-ről és a .F-ről, miszerint a címzésmódok is az utasítás részei, így nem másolódnak át a MOV.F utasítás hatására.

Át is írhatjuk a régi programjainkat úgy, hogy használjuk a módosítókat. Az Imp természetesen MOV.I 0, 1 lesz. A Dwarf pedig:

        ADD.AB #4, 3
        MOV.I  2, @2
        JMP    -2
        DAT    #0, #0

Megjegyezzük, hogy elhagytuk a módosítókat a JMP és a DAT mögül, mert ott nincsenek használva. A MARS egyébként beállítja őket JMP.B-re és DAT.F-re, de kit érdekel?

Jajj, még valami! Honnan tudjuk, hogy milyen utasítás után milyen módosítót kell tenni, pontosabban mit tesz oda a MARS, ha elhagyjuk a módosítót? Általában egy kis érzékkel rá lehet jönni, de azért idetesszük a '94-es standard által definiált alapértelmezéseket.

DAT, NOP
Mindig .F, de nincs figyelembe véve.
MOV, SEQ, SNE, CMP
Ha az A-mód közvetlen, .AB,
ha a B-mód közvetlen, de az A-mód nem, .B,
ha egyik mód sem közvetlen, .I.
ADD, SUB, MUL, DIV, MOD
Ha az A-mód közvetlen, .AB,
ha a B-mód közvetlen, de az A nem, .B,
ha egyik mód sem közvetlen, .F.
SLT, LDP, STP
Ha az A-mód közvetlen, .AB,
ha nem, (mindig!) .B.
JMP, JMZ, JMN, DJN, SPL
Mindig .B. (de a JMP és az SPL nem veszi figyelembe)

Mélyebben '94-es standard-ről

A # több, mint gondolnánk...

A # viselkedésének definíciója a '94-es standard-ben kissé furcsa. Minthogy a standard-nek 100%-ig kompatibilisnek kellett lennie a régi szintaxissal, a közvetlen címzésmódot nagyon okosan és sajátságosan definiálták, ezáltal lehetővé vált az utasítás-módosítók logikus használata, és a címzésmód igen erős eszközzé vált.

Ismerve a módosítókat, lehet, hogy elgondolkozol azon, hogy a MOV.F #7, 10 mit csinál? A .F-nek minkét mezőt mozgatnia kellene, de a forrásban csak egy szám van!? Talán a 7 másolódik a cél minkét mezőjére?

Nem, definíció alapján nem. Az történik, hogy a 7-et átviszi a cél A-mezőjére, a 10-et pedig a B-mezőjére! Miért?

Ennek az az oka, hogy a '94-es standard-ben a forrás (és a cél is) mindig egész utasítás. Közvetlen címzésmód esetén ez egyszerűen az aktuális utasítás (tehát 0), akármi az értéke. Így a MOV.F #7, 10 átmásolja a forrás (0) mindkét mezőjét a célra (10). Meglepő, ugye?

Ugyanígy működik a dolog a MOV.I esetén is. A közvetlen címzés eme definíciójának köszönhetően használhatunk olyan utasításokat is (akár módosító nélkülieket), amiknek a '88-as standardben nem sok értelmük lett volna, például JMP #1234. Természetesen nem ugorhatunk egy számra, de ugorhatunk ennek a számnak a címére, vagyis 0-ra. Ez aztán sok lehetőséget kínál nekünk, nemcsak hogy "ingyen" tárolhatunk adatot az A-mezőn, de még a kód meg sem sérül nagyon, ha valaki csökkenti eggyel az utasítást. Ezek után írjuk át a korábbi imp-készítő kódot kicsit robosztusabbra:

        SPL    #0, }1
        MOV.I  #1234, 1

Ez is ugyanúgy működik, de most az A-mezők szabadok. Csak viccből csináltam, hogy az SPL növelje az imp A-mezőjét, így mindegyik imp egy kicsit más. Minthogy az SPL egyébként nem használja a B-mezőt, ez a növelés szintén "szabad". Hidd el működik - vagy próbáld ki!

Modulo matematika

Már tudnod kell, hogy a core-ban a címek körbefordulnak, vagyis az az utasítás, ami egy core-méretnyivel az aktuális előtt vagy után vannak, az az aktuális utasítás önmaga. Azonban ez a tény sokkal tovább mutat: a Core War-ban minden szám mindig átkonvertálódik a 0 - core-méret-1 intervallumba.

Azoknak, akik már tudnak programozni, és ismerik a gépi egészek matematikáját azt is mondhattam volna, hogy a Core War minden számot előjeltelen egésznek tekint és a legnagyobb tárolható szám a core-méret-1. Ha ez nem teljesen világos, olvass tovább...

Gyakorlatilag a Core War-ban minden szám elosztódik a core hosszával, és csak a maradékot tekintjük. Ez úgy képzelhető el legkönnyebben, hogy van egy számológép, ami 8 számjegyet képes kiírni, és ami ezen felül van azt (a szám elejéről) elhagyja. Így a 100*12345678 (ami 1234567800 lenne) 34567800-ként lesz kijelezve (és eltárolva). Hasonlóan, 8000 utasításos core-ral, 7900+222 (8122) csak 122 lesz.

És mi történik a negatív számokkal? Azok szintén normalizálódnak, mégpedig úgy, hogy a core-méret annyiszorosát adjuk hozzájuk, hogy pozitívak legyenek. Ez azt jelenti, hogy amikor én -1-et írtam, azt tulajdonképpen core-méret-1-ként tárolja a MARS, vagyis a szokásos 8000 utasításos core esetén 7999-ként.

Természetesen ez nem okoz problémát a címeknél, amik egyébként is körmefordulnak. Az egyszerű matematikai műveletek, mint az ADD vagy a SUB is rendben vannak, mivel például core-méret=8000 esetén 6+7998 ugyanúgy 4-et (8004-et) ad eredményül, mint a 6-2.

Akkor mi a probléma? Nos, van néhány utasítás, ahol a dolog változtat a működésen. Ezek a DIV a MOV és az SLT. Ezek is mindig előjeltelenné alakítják a számokat. Ez azt jelenti, hogy -2/2 nem -1, hanem (core-méret-2)/2 = (core-méret/2)-1. (Vagyis core-méret=8000-re 7998/2=3999, nem 7999.) Hasonló okok miatt észleli az SLT a -2-t (7998) nagyobbnak a 0-nál! Ténylegesen a Core War-ban a 0 a lehetséges legkisebb szám, így az összes többi szám nála nagyobb.

A '94-es standard utasításról utasításra

Rendben, a türelmed elnyerte jutalmát. Ezidáig csak információ-részleteket kaptál tőlem. Itt az idő mindent megtudni egyszerre, tehát következnek az utasítások

Persze az itt következőket a legelejére kellett volna vennem, amikor az utasításkészletről volt szó, és ezzel talán megkíméltelek volna egy csomó találgatástól. De nekem (legalábbis szerintem) nagyon jó okom volt várni. Nemcsak azért, hogy az unalmas elméleti fejtegetés előtt mutathassak néhány praktikus kódot, hanem azért is, mert azt akartam, hogy előbb értsd meg a címzésmódok és a módosítók lényegét, és csak aztán az utasításokat részleteikben. Ha az utasításokról a módosítók előtt írtam volna, akkor először a régi '88-as szabályokat kellett volna megtanulnod, aztán az egészet újra a módosítókkal. Nem azt mondom, hogy ez rossz módszer a Redcode megtanulására, de ez szükségtelenül bonyolulttá tette volna ezt az ismertetőt.

DAT
Eredetileg, amint a neve is mutatja, a DAT-ot adattárolásra találták ki, mint ahogy a legtöbb programozási nyelvben van. De minthogy a Core War-ban annál jobb, minél kevesebb utasítással oldunk meg valamit, ezért a pointereket stb. gyakran tároljuk utasítások használatlan mezőjén. Ez pedig azt jelenti, hogy a DAT legfontosabb feladata, hogy végrehajtásával megállíthatunk egy process-t. Természtesen mivel a '94-es standard-ben nincs illegális utasítás, ezért a DAT is teljesen legális, pontosan definiált, a definíciója pedig: törli az éppen végrehajtott process-t a várakozó sorból. Lehet, hogy most kicsit lefárasztottalak, de ugye az utasítások precíz definíciójára a félreértések elkerülése végett igenis szükség van.
A módosítóknak persze semmi hatásuk nincs a DAT-ra, ezért néhány MARS el is hagyta őket. Viszont ne felejtsük el, hogy az utónövelés és az előcsökkentés mindig végrehajtódik, akkor is, ha a mezőt semmire nem használjuk. Egy használhatatlan információ a DAT-ról, ami az előző standard-ekkel való kompatibilitás miatt maradt fenn: ha csak egy argumentuma van, akkor az a B-mezőre kerül.
MOV
A MOV adatot másol egyik címről a másikra. Ha még nem tudsz mindent róla, akkor újra kellene olvasnod a korábbi fejezeteket. A MOV az egyike azon kevés utasításnak, amelyek támogatják a .I módosítót, és ráadásul ez az alapértelmezés, ha nem adunk meg módosítót (és egyik mező sem használ közvetlen címzést).
ADD
Az ADD hozzáadja a forrás értékét/értékeit a cél értékéhez/értékeihez. A módosítók ugyanúgy működnek mint a MOV esetén, kivéve persze, hogy .I nincs támogatva, hanem .F-ként viselkedik. (Mert mi lenne például MOV.AB+DJN.F?) Ezen kívül ne feledjük, hogy a Core War-ban minden matematikai művelet modulo core-méret lesz végrehajva.
SUB
Ez az utasítás pontosan úgy működik mint az ADD, kivéve a nyilvánvaló különbséget. Természetesen az "aritmetikai-logikai" utasítások nagyrészt ugyanúgy működnek...
MUL
...amint ez a MUL-ra is igaz. Ha nem tudod kitalálni mit csinál, akkor valószínűleg kihagytál valami fontosat.
DIV
A DIV szintén úgy működik, mint a MUL és a többiek, de egy pár dolgot észben kell tartanunk. Először is, hogy előjel nélküli osztást hajt végre, ami néha meglepő eredményt szolgáltathat. A nullával való osztás megállítja a process-t, mintha egy DAT hajtódott volna végre, és a cél címen nem változtat. Ha DIV.F-et vagy DIV.X-et használsz két számpár egyidejű elosztására, és az egyik osztó 0, a másik osztás normálisan el lesz végezve.
MOD
Minden, amit a DIV-ről mondtam itt is érvényes, beleértve a nullával való osztást is. Ne feledjük, hogy a MOD.AB #10,#-1 jellegű utasítások eredménye függ a core méretétől. A szokások 8000 utasításos core esetén az eredmény 9 (7999 mod 10)
JMP
A JMP átadja a vezérlést az A-mezőjében megadott címre. A nyilvánvaló, de fontos eltérés a "matematikai" utasításoktól, hogy a JMP csak a címmel törődik, nem az adattal, amire a cím mutat. Egy másik fontos különbség, hogy a JMP nem használja a B-mezőjét (és így a módosítóját sem). Két címre ugrani (vagy egy process-t ugratni, lásd SPL) egyszerre túl erős fegyver lenne, azonkívül a következő három utasítás definiálását is megnehezítené. Ne felejtsük el, hogy rakhatunk növelést vagy csökkentést a nem használ B-mezőbe, ami szerencsés esetben sérülést okozhat az ellenfél kódjában.
JMZ
Ez az utasítás a JMP-hez hasonlóan működik, de ahelyett, hogy figyelmen kívül hagyná a B-mezőjét, megnézi az értéket amire az mutat, és csak akkor ugrik, ha az nulla. Egyébként a végrehajtás a következő utasításon folytatódik. Mithogy csak egy vizsgálandó utaítás van, a választható módosítók száma jelentősen korlátozott. A .AB ugyanazt jelenti mint a .B, a .BA ugyanaz, mint a .A, valamint a .X és a .I ugyanaz, mint a .F. Ha JMZ.F-fel egy utasítás minkét mezőjét vizsgálod, az ugrás csak akkor történik meg, ha mindkét mező nulla.
JMN
A JMN ugyanúgy működik, mint a JMZ, de akkor ugrik, ha a vizsgál érték nem nulla (micsoda meglepetés...). A JMN.F akkor ugrik, ha valamelyik mező nem nulla.
DJN
A DJN olyan mint a JMN, de vizsgált értéket csökkenti eggyel, mielőtt leteszteli azt. Ez az utasítás jól használható ciklusszervezésre, de szokták az ellenfél kódjának elrontására is használni.
SPL
Ez egy nagyon fontos utasítás. Az SPL utasítás bevezetése volt talán a legnagyobb változás a nyelvben, legalább akkora, mint az ICWS '94-es standard bevezetése. Az SPL úgy működik, mint a JMP, de a végrehajtás a következő utasításon is folytatódik, így a process "kettészakad" két új process-re. A következő utasításon lévő process a másik címen lévő előtt hajtódik végre. Ez egy apró, de nagyon fontos részlet (a legtöbb mai harcos nem mőködne enélkül). Ha a program eléri a maximális process-számot, akkor az SPL síma NOP-ként fog működni. Akár csak a JMP, az SPL is figyelmen kívül hagyja a B-mezőjét, és az arra vonatkozó módosítókat.
SEQ
A SEQ összehasonlít két utasítást, és kihagyja a következő utasítást, ha azok egyenlőek. (Mindig csak két utasításnyit ugrik előre mert mér nincs hely az ugrási címnek.) Minthogy az utasítás csak egyenlőséget vizsgál, a .I módosító használható. Teljesen természetes, hogy a .F, .X és a .I módosítókkal a következő utasítás csak akkor lesz átugorva, ha minden mező egyenlő.
SNE
Oké, kitaláltad. Ez az utasítás átugorja a következő utasítást, ha az uatsítások, amiket összehasonlít nem egyenlőek. Ha egynél több mezőt hasonlítasz össze, akkor lesz ugrás, ha valamelyik pár nem egyenlő. (Ismerősen hangzik, nem? Akár a JMZ-nél vagy a JMN-nél...)
CMP
A CPM a SEQ másik neve. A SEQ és a SNE bevezetése előtt ez volt az utasítás egyetlen neve. Manapség nem sokat számít melyik nevet használod, mivel a legtöbb népszerű MARS program a SEQ-t is felismeri '88-as módban is.
SLT
Hasonlóan az előző utasításokhoz, az SLT is átugorja a következő utasítást, ezúttal akkor, ha az első érték kisebb mint a második. Mivel ez egy aritmetikai, és nem egy logikai összehasonlítás, ezért nem reagál az a .I-re. Úgy tűnhet, hogy kellene egy SGT nevű utasítás is (skip if greater than), de a legtöbb esetben a dolog véghez vihető az SLT-vel, operandusok megcserélésével. Ne feledjük, hogy minden érték előjeltelennek lesz tekintve, így a 0 a legkisebb, a -1 pedig a legnagyobb lehetséges szám.
NOP
Nos, ez az utasítás nem csinál semmit (de azt nagyon jól csinálja!). Majdnem soha nem használják kész harcosokban, de nagyon hasznos hibakereséskor. Azért jegyezzük meg, hogy minden előcsökkentés és utónövelés végrehajtódik.

Biztos feltűnt, hogy két utasítás, név szerint az LDP és az STP még hiányzik. Ezek igen új elemei a nyelvnek, és beszélünk is róluk... öööö, nos azonnal. :-)

P-space - a végső határ

A P-space a legutolsó módosítás a Redcode-ban, amit a pMARS 0.8-ban mutattak be. A "P" jelenthet privátot, permanent-et (állandó), personal-t (személyes), patetikusat, és így tovább, amit szeretnél. Alapvetően a P-space egy hely a memóriában, amit csak a te programod érhet el, és aminek tartalma megmarad a fordulók közt többfordulós meccsek esetén.

A P-space több dologban különbözik a normál core-tól. Először is a P-space minden eleme csak egy számot tárolhat, nem egy egész utasítást. Másrészt a címzés a P-space-ben abszolút, azaz az 1-es P-space cím mindig ugyanaz, függetlenül attól, hogy az őt tartalmazó utasítás hol van a core-ban. Végül, de nem utolsósorban, a P-space csak két spciális utasítással érhető el, az LDP-vel és az STP-vel.

A fenti utasítások szintaxisa egy kicsit szokatlan. Az STP például tekinti forrását a szokásos módon, és ezt rakja be a P-space-be, a cél által mutatott helyre. Így a P-space címet nem a célcím határozza meg, hanem a cél értéke, vagyis az az érték, amit az STP felülírna, ha MOV lenne.

Tehát például az STP.AB #4, #5 a 4-es értéket tenné az 5-ös P-space címre. Hasonlóan,

        STP.B  2, 3
	...
        DAT    #0, #10
        DAT    #0, #7

a 10-es értéket a 7-es P-space címre tenné, nem a 3-asra! Ebből szép kis kavarodás lehet, ha az STP maga is indirekt címzést használ, mert ebből "dupla indirekció" lesz.

Az LDP ugyanígy működik, kivéve, hogy most a forrás egy P-space cím, a cél pedig egy core utasítás. A 0-ás P-space cím egy speciális csak olvasható cím. Az oda történő írási kisérletek nem lesznek végrehajtva. Minden kör előtt speciális értéket vesz fel: ez -1, az első körben, 0, ha a program meghalt az előző körben, egyébkent az életben maradt programok száma. Ez annyit jelent, hogy az egy-egy elleni meccsekre 0 jelenti a vereséget, 1 a győzelmet, 2 pedig a döntetlent.

A P-space hossza sokkal rövidebb a core-énál, tipikusan a core méretének 1/16-oda. A címek a P-scape-ben körbefordulnak, akár csak a core-ban. A P-space méretének így érdemes a core-méret hányadosát választani, különben valami borzasztó történik.

Még egy különleges dolog van a pMARS-ban a P-space-ről. Mivel az volt a cél, hogy a P-scape lassú elérésű legyen, így nem engedték meg, hogy egy utasítással két érték legyen elérhető. Ez egy Jó Dolog, bár az eredmény egy kicsit idétlen. Ez itt azt jelenti, hogy az LDP.F, a .X és a .I mind úgy működnek, mint az LDP.B! (És ugyanez érvényes az STP-re is, természetesen.)

A P-space-t messze leggyakrabban a stratégia kiválasztására használják. Ennek legegyszerűbb formája, hogy kimentjük az előző stratégiát a P-space-be, és változtatunk rajta, ha 0-ás P-space cím azt mondja, vesztettünk. Az ilyen programokat hívják P-harcosoknak, P-agyaknak vagy P-keknek. (ejtsd: pékek)

Sajnos a P-space nem olyan privát mint a milyennek tűnik. Mert bár az ellenfeled nem tudja közvetlenül a P-space-ed írni vagy olvasni, elkaphatja a process-edet, és kényszerítheti, hogy az ő kódját hajtsa végre ami pedig tele lehet STP-vel. Ezt a techikát agymosásnak hívják, és minden P-harcosnak védekeznie kell ellene, nem pedig bedilizni, ha a stratégia címen valami szörnyűség van.


A fordító

Címkék és címek

Eddig a példaprogramjaink minden címét az aktuális utasításhoz viszonyított relatív utasításszámként adtuk meg. Ez hosszabb programokban problémás lehet, nem beszélve arról, hogy nehezen átlátható. Szerencsére nem kell így tennünk, mert a Redcode-ban lehetőségünk van címkék, szimbolikus konstansok, makrók és egyéb olyan dolgok használatára, amit az ember egy jó assembler-től elvár. Csak annyit kell tennünk, hogy megcímkézzük az utasításokat és a címkéjükre hivatkozzunk, és a fordító kiszámolja a relatív címet helyettünk. Például:

imp:    mov.i   imp, imp+1

Óhhhh, mi történt? Ez a program pontosan ugyanaz, amit a legelején már láttunk. Csak éppen a numerikus címeket az "imp" címkére cseréltem. Persze jelen esetben ez nem nagyon szükséges. Az egyetlen utasítás, amelyikben a címkét használtuk az "imp" maga, így a címke 0-ra cserélhető.

A végrehajtás előtt, a MARS fordítója az összes címkét és egyéb szimbólumot a már ismert számokra változtatja. Az ilyen "előfordított" Redcode file neve load file, mivel minden MARS ismeri a load file formátumot, de nem mindnek van tényleges fordítója. (load = betöltés) Load file formátumban az előző kód MOV.I 0, 1 lesz. Ugyanezt a kódot megírhatjuk így is:

imp:    mov.i   imp, next
next:   dat     0, 0            ; vagy bármi

Ebben az esetben a "next" cíkéjű utasítás egy utasítással az "imp" után van, így 1-re cserélődik. Emlékezzünk rá, hogy a valódi címek továbbra is relatív számok, így az Imp azután is MOV.I 0, 1 miután előremásolta magát a "next"-re.

A : a címkék után tulajdonképpen nem szükséges. Azért használtam, hogy könnyebben lásd, hol vannak a címkék, de általában nem használom őket saját programjaimban. Ez ízlés dolga.

Ja, és csak ha érdekel, a Redcode nem érzékeny a kis- és nagybetűkre. (Ez így, ebben a formában nem igaz. A címkék ugyanis kis-nagybetű érzékenyek. Az utasítások valóban nem. Megj. a fordítótól.) Én úgy szeretem, hogy kisbetűt használok a forrásokban, mert az úgy szebb, és a nagybetűket csak a "load file" formában. (Leginkább, mert ez a szokás.)

A teljes program

Igaz, hogy az előző fejezetek példái, általában lefordíthatók, de mégsem igazán kész programok, csak azok részei. Egy tipikus Redcode file tartalmaz néhány extra információt a MARS-ról.

;redcode-94
;name Imp
;author A.K. Dewdney

        org     imp

imp:    mov.i   imp, imp+1
        end

Amint azt valószínűleg kikövetkeztetted, minden ami a ; után van az a Redcode-ban megjegyzés. E program első sorai mégsem szokásos megjegyzések. A MARS arra használja őket, hogy néhány információt kiszűrjön belőlük a programról.

Az első sor, a ;redcode-94 jelenti, hogy ez tényleg egy Redcode file. Minden, ami ez előtt a sor előtt van, szintén megjegyzés. Igazából a MARS csak egy olyan sort keres, ami úgy kezdődik, hogy ;redcode, és így a sor maradék része használható a Redcode fajtájának azonosítására. Például a KotH szerverek beolvassák ezt a sort maguknak, és ebből azonosítják, hogy a program melyik hegyre tart.

A ;name (=név) és az ;author (=szerző) sorok csak információkat adnak a programról. Persze ezeket bármilyen formában odaírhatod, de ha ezt a formát használod, akkor a MARS megtalálja sorokat, és pl. kijelezheti az információt, mikor a program fut.

A sor, amelyikben az END van, a program végét jelzi. Minden, ami utána van, szintén megjegyzés. Együtt a ;redcode-dal használahtók például arra, hogy belerakj egy Redcode programot egy e-mail-be.

A sor, amiben az ORG van, megmutatja, hogy hol kezdődjön a program végrehajtása. Így újabb utasításokat tehetünk a program eleje elé. Az ORG parancs a '94-es standard újításainak egyike. A régebbi szintaxisban ami még mindig használahtó modern programokban is, a kezdőcímet az END argumentumaként adták meg.

;redcode-94
;name Imp
;author A.K. Dewdney

imp:    mov.i   imp, imp+1

        end     imp

Egyszerű, tömör, kár, hogy egy kissé illogikus. Azon kívül hosszú programoknál azok végére kell menned, hogy meglásd, hogy hol kezdődnek. A Redcode terminológiában az ORG és az END neve pszeudo-utasítás. Rendes utasításoknak néznek ki, de nem fordítódnak bele a programba.

De elég az Imp-ből. Nézzük, hogy nézne ki a Dwarf modern Redcode-ban:

;redcode-94
;name Dwarf
;author A.K. Dewdney
;strategy Bombs the core at regular intervals.
;(A fenti sor magyarul: egyenlő intervallumokban bomázza a core-t.)
;(kissé módosította: Ilmari Karonen)
;assert CORESIZE % 4 == 0

        org     loop

loop:   add.ab  #4, bomb
        mov.i   bomb, @bomb
        jmp     loop
bomb:   dat     #0, #0

        end

Ezek a címkék sokkal érthetőbbé teszik a programot, nem? Megfigyelhető, hogy újabb két megjegyzés sort tettem hozzá a programhoz. A ;strategy sor röviden leírja a programot. Akárhány ilyen sor lehet a programban. A legtöbb jelenlegi MARS kihagyja őket, szóval hagyományos megjegyzést is használhatsz erre (mint annál a sornál, amiben a nevem van), de a Hegyek kijelzik a ;strategy sorokat másoknak. Ha az előbbi programot elküldjük az egyikre, valami ilyesmi lesz a többieknek kijelezve:

A new challenger has appeared on the '94 hill!

Dwarf by A.K. Dewdney: (length 4)
;strategy Bombs the core at regular intervals.

[egyéb információ...]
Új program jelent meg a '94 hegyen! Dwarf, írta A.K. Dewdney: (hossz 4) ;strategy Bombs the core at regular intervals. [egyéb információ...]

A környezet és a ;assert

A másik új elem a példánkban a ;assert sor. Arra használjuk, hogy biztosak lehessünk abban, hogy a program tényleg működik az érvényben lévő beállításokkal. A Dwarf például megöli magát, ha a core mérete nem osztahtó 4-gyel. Ezért használtam a ;assert CORESIZE % 4 == 0-t, hogy biztos lehessek benne, hogy igaz.

A CORESIZE előre definiált konstans, ami a core hosszát tartalmazza. Ekkor az n+CORESIZE mindig ugyanaz a cím, mint az n. A % a modulus operátor, ami az osztásnál keletkező maradékot adja. Az ;assert sorokban és máshol a Redcode-ban használt kifejezések szintaxisa ugyanaz, mint a C-nyelvben, bár az operátorkészlet jelentősen korlátozott.

Azoknak, akik nem tudnak C-ben, íme egy lista a Redcode kifejezésekben használható operátorokról:

Aritmetikai:
+ összeadás
- kivonás (vagy negáció)
* szorzás
/ osztás
% modulus (maradék)
Összehasonlítás:
== egyenlő
!= nem egyenlő
< kisebb mint
> nagyobb mint
<= kisebb vagy egyenlő
>= nagyobb vagy egyenlő
Logikai:
&& és
|| vagy
! nem
Értékadás:
= értéket ad egy változónak

Az ;assert-et egy logikai kifejezés követi. Ha ez hamis, a programot le sem fordítják. C-ben a 0 érték hamisat jelent, minden más igazat. A logikai és az összehasonlító operátorok minden igaz értékre 1-et adnak vissza, ami később hasznos lesz nekünk.

Az ;assert tipikus használata, hogy leellenőrizzük, hogy a core hossza annyi-e, mint amekkora core-ra programot terveztük, pl. ;assert CORESIZE==8000. Ha a program P-space-t használ, akkor a ;assert PSPACESIZE > 0 paranccsal leellenőrizhetjük, hogy van-e P-space. Minthogy a péladprogramunk, a Dwarf, elég alkalmazkodó, így csak a CORESIZE oszthatóságát teszteltem, nem a konkrét méretét. Az Impnek, ami bármilyen beállítással elfut, ;assert 1-et vagy ;assert 0==0-t szokás megadni, vagy valamit, ami mindig igaz értéket ad. Ez azért hasznos, mert különben a MARS "missing ;assert line -- warrior may not work with the current settings." (hiányzó ;assert sor -- a harcos lehet, hogy nem működik az aktuális beállításokkal.) üzenetet ad.

Néhány előre definiált konstansot, mint pl. a CORESIZE-ot, még a '94-es standard-ben definiáltak, utána még néhany továbbit hozzátettek. A pMARS 0.8 minimum a következőket ismeri:

#define? Hát, majdnem...

Az előre definiált konstansok hasznosak, meg a címkék is, de ennyi az egész? Nem lehet változókat, vagy valami hasonlót használni?

Hát, a Redcode assembly nyelv, és nem igazán használ változókat. De van valami, ami majdnem olyan jó, sőt, esetenként jobb is. Az EQU pszeudo-utasítással definiálhatunk saját konstansokat, kifejezéseket, vagy akár makrókat is. Ilyenformán:

step    equ     2667

Ezután a step mindig kicserélődik 2667-re. Van azonban egy csapdája a dolognak: a csere szöveges, nem numerikus. Ebben az esetben ez nem okazhat gondot, ám miközben ez az EQU-t komoly eszközzé teszi, okozhat néhány problémát, amit a C programozók már jól ismernek. Vegyünk egy példát:

step    equ     2667
target  equ     step-100

start   mov.i   target, step-target

A MOV A-mezője 2567 lesz, ahogy vártuk. Ám a B-mező értéke 2667-2667-100 == -100 lesz, nem pedig 2667-(2667-100) == 2667-2567 == 100, ahogy azt valószínűleg akartuk volna. A megoldás egyszerű. Tegyünk zárójelet az összes EQU-s kifejezés köré, pl. "target equ (step-100)".

A pMARS modern verzióiban megengedett a többsoros equ használata is, így létrehozhatunk hosszabb makrókat. Íme a módszer:

dec7    equ     dat #1, #1
        equ     dat $1, $1
        equ     dat @1, @1
        equ     dat *1, *1
        equ     dat {1, {1
        equ     dat }1, }1
        equ     dat <1, <1

decoy   dec7
        dec7            ; 21 utasításos csapda
        dec7

Mire hasznájuk a "rof"-ot?

Van még a pMARS fordítónak néhány képessége ami eddig kimaradt. Ami most következik talán erősebb eszköz (és bonyolultabb), mint bármelyik másik. A FOR ROF pszeudo-utasítások nemcsak rövidebbé teszik a forrást és könnyebben lehet velük komplex kódsorozatot létrehozni, hanem a segítségükkel készíthetünk feltételes kódot különböző beállításokhoz.

A FOR blokk - igen, kitaláltad - a FOR pszeudo-utasítással kezdődik, majd egy szám következik: az, hogy hányszor kell a blokkot megismételni. Ha a blokk előtt címke van, akkor az ciklusváltozóként lesz használva:

index   for     7
        dat     index, 10-index
        rof

Amint látható, a blokk ROF-fal végződik. (Szerintem sokkal jobb, mint a régi NEXT vagy REPEAT klisék.) Az előző blokkot a pMARS a következőnek fordítja:

        DAT.F   $1, $9
        DAT.F   $2, $8
        DAT.F   $3, $7
        DAT.F   $4, $6
        DAT.F   $5, $5
        DAT.F   $6, $4
        DAT.F   $7, $3

Lehetőségünk van több FOR blokk egymásba ágyazására. A blokkok akár EQU-kat is tartalmazhatnak; ezzel néhány nagyon érdekes kódot írhatunk. Egy másik hasznos dolog, hogy a ciklusszámláló hosszáfűzhető egy címkéhez is a & operátor segítségével. Ezt gyakran arra használjuk, hogy elkerüljük egy címke többszöri deklarálását, de számos más célra is hasznos lehet.

dest01  equ     1000
dest02  equ     1234
dest03  equ     1666
dest04  equ     (CORESIZE-1111)

jtable
ix      for     4
jump&ix spl     dest&ix
        djn.b   jump&ix, #ix
        rof

Ez a FOR/ROF lefordítása után a következő lesz:

jtable
jump01  spl     dest01
        djn.b   jump01, #1
jump02  spl     dest02
        djn.b   jump02, #2
jump03  spl     dest03
        djn.b   jump03, #3
jump04  spl     dest04
        djn.b   jump04, #4

Hogy mire használható még, az csak a képzeletedtől függ. Az egyetlen harcosok, amiket én ilyen komplex kifejezéseket használni láttam, az néhány quickscanner (gyorskereső) volt. Az előredefiniált konstansok szintén használhatók a FOR/ROF-fal, például:

; A harcos fő része itt van

decoy
foo     for     (MAXLENGTH-CURLINE)
        dat     1, 1
        rof

        end

Az előzőek feltöltik a harcosod maradék részét DAT 1, 1-gyel. Az ilyen csapda eltérítheti más harcosok támadását, ha gondoskodsz róla, hogy a programodat elmásold (boot-old) a csapdától távolra. Megjegyezném, hogy definiáltam egy foo nevű ciklusváltozót, holott nem használtam semmire. Ezt azért tettem, mert különben a MARS a decoy-t ciklusváltozónak hitte volna, pedig az címke.

;redcode-94
;name Tricky
;author Ilmari Karonen
;strategy Valami nagyon bonyolult bizbaz harcos
;strategy (A feltételes kód önmagyarázó példája)
;assert CORESIZE == 8000 || CORESIZE == 800
;assert MAXPROCESSES >= 256 && MAXPROCESSES < 10000
;assert MAXLENGTH >= 100

        org     start

        for     0
Ez itt egy for/rof megjegyzés blokk. Nullaszor lesz ismételve,
ami annyit jelent, hogy a MARS minden itt lévő dolgot kihagy.
Ez egy tökéletes hely arra, hogy elmagyarázzuk azt a bonyolult
stratégiát, amit a harcos használ.
        rof

;Természetesen hagyományos megjegyzések használata is megengedett.
;Mindig azt a módszert használod, amelyiket akarod.

        for     (CORESIZE == 8000)
step    equ     normalstep
;Mivel az igaz összehasonlítás eredménye 1, a hamisé pedig 0, ez a
;kód akkor lesz lefordítva, ha az összehasonlítás igaz.
        rof

        for     (CORESIZE == 800)
step    equ     tinystep
;Ide pedig berakhatjuk a kisebb core-ra optimalizált konstansokat.
        rof

        for     0
;strategy Mivel a strategy és az assert sorok valódi megjegyzések,
;strategy ezért még FOR 0 / ROF blokkban is le lesznek fordítva!
        rof

;[A tényleges kód helye..]

Változatok változókra

A probléma az EQU által definiált konstansokkal az, hogy, hmmm, konstansok. Ha egyszer definiáltad őket, akkor nem tudod megváltoztatni az értéküket. A legtöbb esetben jók, de néhány dologra szinte lehetetlen használni őket.

Szerencsére a pMARS lehetőséget ad egy pár valódi változó használatára is. A használatuk egy kicsit trükkös, és nagyon rég nem láttam senkit használni őket, de tényleg léteznek.

A változónév mindig egybetűs, effektíve lekorlátozva a számukat 26-ra (a-tól z-ig). Az EQU használata helyett az értékadás a = operátorral történik. A trükk az, hogy az operátor csak kifejezésban használható. És mivel a pMARS nem ismeri a vesszőoperátort, szükség lehet egy kamu kifejezés írására.

Persze a változók így is hasznosak. Például a következő automatikusan generált Fibonacci-sorozatot nem lehetne megírni nélkülük.

        dat     #1, (g=0)+(f=1)
idx     for     15
        dat     #idx+1, (f=f+g)+((g=f-g) && 0)
        rof

Megjegyzendő, hogy a (g=f-g) kifejezés a 0-val való AND-elés miatt "rejtett" lesz. A rendszer működik, mert a pMARS nem változtat a műveletek sorrendjén, hanem az összeadásnak mindig a bal oldalát értékeli ki először, és mire a jobb oldalt számolja, az f értékét már megnövelte.

PIN és pincsi

Oké, majdnem elfelejtettem. Van még egy pszeudo-utasítás, ami kimaradt. Szinte soha nem használatos, de igen, van. Ez a PIN, ami "P-space Identification Number"-t, azaz "P-space azonosító szám"-ot jelent. Ha két programnak ugyanaz a PIN-je, akkor a P-space-ük közös lesz. Ez a process-ek közötti kommunikációra vagy kooperációra ad lehetőséget. Sajnos a módszer nem tűnik eredményesnek a gyors és hatékony kommunikáció megvalósítására. Persze ha meg akarod próbálni, hát rajta. Sosem lehet tudni, sikerül-e...

Ha a programnak nincs PIN-ja, akkor a P-space-e mindig privát lesz. De ha esteleg két program osztozik a P-space-en, a speciális csak olvasható 0-ás cím mindig privát lesz.

Másszunk hegyet

Ha még nem tudsz róla, a King of the Hill (A Hegy Királya) szerverek folyamatos Core War versenyek az Interneten. A harcosokat e-mailben küldik a szerverre, ahol megküzdenek az összes (általában 10-30) hegyen lévő programmal. Az a program, amelyik a legkevesebb pontot gyűjti összesen, leesik a hegyről, ahol az új harcos váltja fel. (Aki jobb pontszámot ér el legyalább az egyik eredeti programnál.)

Jelenleg két KotH szerver fut, mindkettő számos különböző heggyel: a StormKing KotH szerver a koth@koth.org-on és az Internet Pizza KotH szerver a pizza@ecst.csuchico.edu-on. Minkettő használ sok olyan parancsot, amiről nem volt szó, leginkább speciális megjegyzéssorokat, mint a ;kill vagy a ;password. Mivel azonban ezek a parancsok szerverenként különbözőek, érdemes átnézni a saját dokumentációikat információkért.

Megjegyezzük, hogy a hegyek, időmegtakarítás végett általában előre lefordítják a harcosokat load file-okká és ezt a formát tárolják, futtatják. Ez az oka annak, hogy némely előre definiált konstans, pl. a WARRIORS, szabálytalan, ami rejtélyes ;assert hibát eredményez.


Történet

0.50
A fordítóról szóló fejezet befejezése. (1997. március 25.)
0.51
A for/rof példákban lévő hiba kijavítása
0.52
Az első publikált változat
0.53
Néhány elgépelés és helyesírási hiba kijavítása
0.54
A '88 -> '94 szabályokkal való kiegészítés
0.55
A HTML szebbé tétele
1.00
Információ az = operátorról. Nevezhetnénk az "1. verziónak" is... (1997. május 5.)
1.01
A <DD>-kkel való inkompatibilitás kijavítása.
1.02
Néhány elgépelés és illogikus mondat kijavítása. Az oldalon való navigáció egységesre hozása.
1.03
Képek eltávolítása, a dokumentum típusa Strict.
1.10
Aaaah! Végig fordítva írtam az SLT-t! Kijavítva. (1998. március 8.)
1.10
Magyar fordítás. (1999. július 13.)