Redcode 入門ガイド(初心者向け), v1.23(日本語訳)
Redcode 入門ガイド(初心者向け)
バージョン 1.23
最近は Core War というゲームに興味を持つ初心者が、あまり多くありません。もちろん、これは自然なことです。アセンブリコードの最適化を楽しいと感じる人は、そもそも多くないからです。しかし、出発点の敷居が高い理由の 1 つに、「ゲームの基本中の基本」についての情報が見つけにくいこともあります。良い文書は多く存在しますが、技術的すぎる、古い、探しにくい、あるいは不完全といった問題が起こりがちです。
そこで私はこのガイドを書くことにしました。目的は、Core War と Redcode に初めて触れる人を、動く(勝てなくてもよい)ウォリアーを書けるところまで案内し、さらに技術的な資料へ進めるようにすることです。
正直に言えば、私自身もまだこのゲームの初心者です。言語についてはかなり理解していますが、本当に成功したウォリアーをまだ作れていません。ただ、経験を積むまで待つのではなく、「新規プレイヤーとして、ゲームの癖を理解しようとして苦労する感覚」を鮮明に覚えているうちに、できるだけ早く書くことにしました。
このガイドは 本当に 初心者向けです。アセンブリ言語(あるいはプログラミング一般)の知識は不要で、概念をうっすら知っている程度なら用語理解の助けになります。Redcode(特に現代の仕様)は見た目こそアセンブリ風ですが、多くの点でより抽象的で、他のアセンブリ言語とは細部がかなり異なります。
このガイドで扱う Redcode は(概ね)現在の de facto 標準である ICWS ’94 Standard Draft に pMARS 0.8 拡張を加えたものです。(HTML に対する Netscape 拡張のようなものです…うーん…。幸い、Microsoft Corewar Simulator はまだありません。市場が小さすぎると思っているのでしょう。)古い ’88 標準についても軽く触れますが、中心は ’94 標準です。’88 を学びたい人向けのチュートリアルは Web にたくさんあります。
重要:Redcode(あるいはどんなプログラミング言語でも)を、完全に直線的な順序で教える簡単な方法はありません。このガイドも、ある程度は筋の通った順序に整理しましたが、飛び飛びに読んでも全く構いません。そのための 目次 です。
全体の整合性を保つために、先に例を見せてから数章後で説明する、という構成がしばしば登場します。理解しにくい点があれば、少し先まで読み進めてください。それでも分からなければ、別の章で説明されていないか探してみてください。
学び方は人それぞれなので、あなたが選ぶ読み順は、私が選んだ順序より良い可能性があります。ただし、退屈だからと完全に読み飛ばすと、重要な情報を取り逃がすことがあります。重要な箇所には 強調 を付けるよう努めました。とはいえ、すべてを逐一書き切ると、このガイドは長くなりすぎて読めなくなります。できるだけ丁寧に読んでください。
Core War(または Core Wars)は、シミュレートされたコンピュータのメモリ上で、アセンブリプログラム同士が互いを破壊し合うプログラミングゲームです。プログラム(warriors=ウォリアー)は Redcode という特別な言語で書かれ、MARS(Memory Array Redcode Simulator)という実行環境で動作します。
Redcode と MARS の環境は、普通のコンピュータシステムに比べて大きく単純化され、抽象化されています。これは良いことです。Core War のプログラムは可読性ではなく性能のために書かれます。もし通常のアセンブリ言語を使うゲームだったら、世界で 2〜3 人しか「効果的で丈夫な」ウォリアーを書けず、しかも本人たちですら完全には理解できないかもしれません。可能性は大きいものの、そこそこに到達するだけでも何年もかかるでしょう。
プログラムが走るシステムはとても単純です。core(シミュレートされたコンピュータのメモリ)は連続した命令の配列で、競技プログラム以外は空です。core は環状に繋がっており、最後の命令の次は最初の命令に戻ります。
実際、プログラムは core の終端を知る手段がありません。絶対アドレスが存在しないからです。つまり、アドレス 0 は「メモリの最初の命令」ではなく、「そのアドレスを含む命令自身」を意味します。次の命令は 1、前の命令は -1 です。
ご覧の通り、Core War のメモリの基本単位は通常の 1 バイトではなく「1 命令」です。各 Redcode 命令は 3 つの部分からなります。OpCode(命令そのもの)、ソースアドレス(別名 A-field)、デスティネーションアドレス(B-field)です。A-field と B-field の間でデータを移すことも可能ですが、基本的には命令を分割不能なブロックとして扱う必要があります。
実行も同様に単純です。MARS は 1 サイクルに 1 命令を実行し、命令が別アドレスへのジャンプを指示しない限り次の命令へ進みます。複数のプログラムが走る場合(通常そうです)、各プログラムは交互に 1 命令ずつ実行されます。MOV でも DIV でも、プロセスを殺す DAT ですら、実行時間は等しく 1 サイクルです。
Redcode の命令数は標準が新しくなるたびに増え、最初は 5 つ程度だったものが、現在は 18 か 19 になりました。しかも修飾子とアドレッシングモードが増え、組み合わせは文字通り数百になります。とはいえ、全部を暗記する必要はありません。命令と、修飾子がどう作用を変えるかを覚えれば十分です。
Redcode で使われる命令の一覧です:
DAT — data(データ。実行するとプロセスを殺す)
MOV — move(あるアドレスから別アドレスへコピー)
ADD — add(加算)
SUB — subtract(減算)
MUL — multiply(乗算)
DIV — divide(除算)
MOD — modulus(剰余。割って余りを得る)
JMP — jump(別アドレスへ制御移動)
JMZ — jump if zero(値を調べ、0 ならジャンプ)
JMN — jump if not zero(0 でなければジャンプ)
DJN — decrement and jump if not zero(1 減算し、結果が 0 でなければジャンプ)
SPL — split(別アドレスで第 2 のプロセスを開始)
CMP — compare(SEQ と同義)
SEQ — skip if equal(2 命令を比較し、等しければ次命令をスキップ)
SNE — skip if not equal(等しくなければ次命令をスキップ)
SLT — skip if lower than(2 値を比較し、第 1 が 第 2 より小さければ次命令をスキップ)
LDP — load from p-space(プライベート領域から読み込む)
STP — save to p-space(プライベート領域に書き込む)
NOP — no operation(何もしない)
いくつかが(控えめに言って)妙に見えても心配しないでください。Redcode は抽象的なので、一般的なアセンブリ言語とはかなり違います。
真実として、Redcode でいちばん重要な部分は、いちばん簡単な部分です。基本的なウォリアーの多くは、新しい命令やモードが登場する前に発明されました。最も単純で、おそらく最初の Core War プログラムは Imp です。A. K. Dewdney が 1984 年の Scientific American の記事で Core War を一般に紹介した際に掲載されました。
MOV 0, 1
はい、これだけです。たった 1 つの MOV。では、これは何を する のでしょうか? MOV は命令をコピーします。Core War のアドレスは現在命令からの相対なので、Imp は自分自身を「自分の直後の命令」にコピーします。
MOV 0, 1
MOV 0, 1
Imp は、いま書いた命令を次に実行します。全く同じ命令なので、また 1 命令先へ自分をコピーし、そのコピーを実行し…という動きを繰り返し、core を MOV で埋めながら前進します。core には終端がないので、全域を埋めた後は起点に戻り、ad infinitum(永遠に)ぐるぐる回り続けます。
つまり Imp は、実行しながら自分のコードを生成します! Core War では自己書き換えは例外ではなくルールです。勝つには効率が必要で、ほぼ必ず「その場でコードを変える」ことになります。抽象化された環境のおかげで、通常のアセンブリより追いやすいのが救いです。
ちなみに、Core War にキャッシュはありません。まあ正確には、現在 実行中の命令だけはキャッシュされるので、実行の 途中 でそれを変えることはできませんが、その話は後にしましょう…。
Imp にはウォリアーとして小さな欠点があります。勝ちにくいのです。相手を上書きしても、相手も MOV 0, 1 を実行し始めてインプ化し、引き分けになりがちです。相手を殺すには DAT を相手コードにコピーする必要があります。
そこで Dewdney の別の古典ウォリアー Dwarf(ドワーフ)が登場します。これは core を一定間隔で DAT で「爆撃」し、自分に当たらないようにしながら相手を殺します。
ADD #4, 3
MOV 2, @2
JMP -2
DAT #0, #0
これは Dewdney の原文と寸分違わずではありませんが、動きは同じです。実行は先頭命令から始まります。最初は ADD です。ADD はソースとデスティネーションを足し、結果をデスティネーションへ置きます。他のアセンブリ言語を知っているなら、# は即値(immediate)を示す記号として見覚えがあるでしょう。つまりこの ADD は「アドレス 3 の命令」に 4 を足します。「命令 4 を命令 3 に足す」ではありません。ADD の 3 つ後の命令は DAT なので、結果は次の通りです:
ADD #4, 3
MOV 2, @2
JMP -2
DAT #0, #4
命令どうしを足す場合、A-field と B-field は独立に加算されます。1 つの数値を命令に足す場合、デフォルトでは B-field に加算されます。ADD の B-field にも # を付けられます。その場合、A-field が ADD 自身の B-field に足されます。
即値アドレッシングは簡単で馴染み深く見えますが、ICWS ’94 の新しい 修飾子 が、これに 全く新しいひねり を加えます。ですが、まずはドワーフを見ていきましょう。
MOV には別のアドレッシングモードが出てきます。@ は間接(indirect)です。見た目通りに DAT を自分自身へコピーする(それで何が嬉しいのか?)のではなく、「B-field が指す先の命令」へコピーします:
ADD #4, 3
MOV 2, @2
JMP -2
DAT #0, #4
...
...
...
DAT #0, #4
ご覧の通り、DAT は自分から 4 命令先にコピーされます。次の JMP は 2 命令後ろへ飛び、ADD へ戻します。JMP は B-field を無視するので空にしました。MARS は 0 で初期化します。
なお、MARS は間接参照の鎖をさらに辿りません。間接オペランドが指す先が B-field=4 の命令であれば、実際の宛先は「そこから 4 命令先」になり、アドレッシングモードは関係ありません。
さて ADD と MOV が再び実行されます。実行が JMP に戻ったとき、core は次のようになっています:
ADD #4, 3
MOV 2, @2
JMP -2
DAT #0, #8
...
...
...
DAT #0, #4
...
...
...
DAT #0, #8
ドワーフは core を一周して自分の位置に戻るまで、4 命令おきに DAT を落とし続けます:
...
DAT #0, #-8
...
...
...
DAT #0, #-4
ADD #4, 3
MOV 2, @2
JMP -2
DAT #0, #-4
...
...
...
DAT #0, #4
...
この状態で ADD は DAT を再び #0, #0 に戻し、MOV は無意味にも DAT を「既にある場所へ」コピーし、全体が最初から繰り返されます。
これは core のサイズが 4 で割り切れないと成立しません。そうでないと、ドワーフは DAT の 1〜3 命令手前を踏んで自滅します。幸い、よく使われる core サイズは 8000 が最も一般的で、次いで 8192、55400、800 などで、いずれも 4 で割り切れるので、このドワーフは安全です。
補足として、ウォリアーに DAT #0, #0 を含める必要は本当はありません。core が最初に埋められている命令(ここでは三点リーダ ... と書いたもの)は実際には DAT 0, 0 です。読みやすいので、以後も空の core を三点リーダで表します。
Core War の初期バージョンでは、アドレッシングモードは即値(#)、直接($ または無指定)、B-field 間接(@)の 3 つだけでした。後に、プリデクリメント(predecrement)である < が追加されました。これは間接と同じですが、ターゲットアドレスを計算する前にポインタを 1 減らします。
DAT #0, #5
MOV 0, <-1
この MOV が実行されると、結果は次のようになります:
DAT #0, #4
MOV 0, <-1
...
...
MOV 0, <-1
ICWS ’94 Standard Draft では、主に A-field の間接参照に対応するために 4 つのモードが追加され、合計 8 モードになりました:
# — 即値(immediate)
$ — 直接(direct。$ は省略可)
* — A-field 間接(A-field indirect)
@ — B-field 間接(B-field indirect)
{ — A-field 間接+プリデクリメント
< — B-field 間接+プリデクリメント
} — A-field 間接+ポストインクリメント
> — B-field 間接+ポストインクリメント
ポストインクリメントはプリデクリメントに似ていますが、命令が実行された 後 にポインタが 1 増えます。
DAT #5, #-10
MOV -1, }-1
は、実行後に次のようになります:
DAT #6, #-10
MOV -1, }-1
...
...
...
DAT #5, #-10
プリデクリメント/ポストインクリメントで覚えておくべき重要点は、ポインタは「実際に参照に使われない場合でも」増減することです。たとえば JMP -1, <100 は、値を使わないのに命令 100 をデクリメントします。DAT <50, <60 も、プロセスを殺すだけでなくアドレスをデクリメントします。
上の命令表をよく見た人は SPL に気づいたかもしれません。普通のアセンブリ言語にはまずありません…。
Core War の歴史のかなり初期に、「マルチタスクを入れればもっと面白くなる」という提案がありました。通常の OS のタイムスライス方式は抽象環境に合わない(何より OS が必要)ため、各プロセスを 1 サイクルずつ順番に実行する方式が考案されました。
新しいプロセスを作る命令が SPL です。A-field にアドレスを取り、JMP と同様に見えます。違いは、SPL は新しいアドレスから実行を開始する だけでなく、次の命令でも実行を 継続する 点です。
こうして生まれた 2 つ以上のプロセスは、同じ実行時間を等しく分け合います。単一のプロセスカウンタではなく、MARS は process queue(プロセスキュー)という「開始順に繰り返し実行されるプロセスのリスト」を持ちます。SPL で作られた新プロセスは「現在のプロセスの直後」に追加され、DAT を実行したプロセスはキューから削除されます。全プロセスが死ぬと、そのウォリアーは負けです。
重要なのは、各プログラムが 自分専用の プロセスキューを持つことです。core に複数のプログラムがある場合、各プログラムはキューの長さに関係なく、常に 1 サイクルずつ交互 に実行されます。したがって処理時間は常に等分されます。たとえば プログラム A が 3 プロセス、プログラム B が 1 プロセスなら、実行順は次のようになります:
- プログラム A、プロセス 1、
- プログラム B、プロセス 1、
- プログラム A、プロセス 2、
- プログラム B、プロセス 1、
- プログラム A、プロセス 3、
- プログラム B、プロセス 1、
- プログラム A、プロセス 1、
- プログラム B、プロセス 1、
...
最後に、SPL の小さな例です。より詳しい話は後の章で扱います。
SPL 0
MOV 0, 1
SPL が自分自身を指しているので、1 サイクル後にはプロセスはこうなります:
SPL 0
MOV 0, 1
両方のプロセスが実行されると、core は次のようになります:
SPL 0
MOV 0, 1
MOV 0, 1
つまりこのコードは、次々に Imp を発射していくのが分かります。Imp たちが core を一周して SPL を上書きするまで続きます。
各プログラムのプロセスキューには上限があります。最大プロセス数に達している場合、SPL は 次の命令だけ で実行を継続し、結果として NOP と同じ挙動になります。多くの場合、上限は core の長さと同程度でかなり大きいですが、もっと小さく設定されることもあります(1 の場合は分割が実質無効です)。
それと、「現実は小説より奇なり」という話として、私は最近 "Opcodes that should've been" というページを見ました。そこには荒唐無稽な命令がいろいろあり、その中に "BBW — Branch Both Ways" がありました。架空の命令のはずなのに、作者は Redcode を知らなかったのだろうとしか思えません…。
ICWS ’94 標準がもたらした最重要の新要素は、新命令や新アドレッシングモードではなく「修飾子」です。古い ’88 標準では、アドレッシングモードだけが「命令のどの部分が操作対象か」を決めます。たとえば MOV 1, 2 は常に命令全体を移動し、MOV #1, 2 は数値 1 つだけを移動します(しかも 常に B-field へ)。
当然、これでは困る場面が出ます。OpCode を変えずに A/B フィールドだけ移したい場合は?(ADD を使う必要があります) B-field から A-field に移したい場合は?(可能ですが、とても厄介) そこで状況を明確化するために、修飾子が導入されました。
修飾子は命令に付ける接尾辞で、ソースとデスティネーションのどの部分に作用するかを指定します。たとえば MOV.AB 4, 5 は「アドレス 4 の A-field を、アドレス 5 の B-field に移す」ことを意味します。修飾子は 7 種類あります:
MOV.A — ソースの A-field をデスティネーションの A-field へ移す
MOV.B — ソースの B-field をデスティネーションの B-field へ移す
MOV.AB — ソースの A-field をデスティネーションの B-field へ移す
MOV.BA — ソースの B-field をデスティネーションの A-field へ移す
MOV.F — ソース両フィールドを、同じ対応フィールドへ移す
MOV.X — ソース両フィールドを、逆対応フィールドへ移す
MOV.I — ソース命令全体をデスティネーションへ移す
当然ながら、これらの修飾子は MOV だけでなく全命令で使えます。ただし JMP や SPL のように、修飾子を気にしない命令もあります。(データを扱わず、ジャンプするだけだからです。)
すべての修飾子がすべての命令で意味を持つわけではないので、意味が通る「最も近いもの」にフォールバックします。よくある例が .I です。Redcode は単純で抽象的な言語にするため、OpCode の数値的な等価物を定義していません。従って OpCode に数学演算をかける意味がありません。このため MOV、SEQ、SNE(および別名の CMP)以外では .I は .F と同義になります。
.I と .F についてもう 1 点。アドレッシングモードも OpCode の一部であり、MOV.F ではコピーされません。
例として、古いプログラムを修飾子付きで書き直してみます。Imp は自然に MOV.I 0, 1 になります。Dwarf はこうです:
ADD.AB #4, 3
MOV.I 2, @2
JMP -2
DAT #0, #0
JMP と DAT は修飾子を実質使わないので省略しています。MARS は(例として)JMP.B や DAT.F に補うかもしれませんが、あまり気にしなくてよいでしょう。
もう 1 つ。どうやってどの修飾子を付けるか(また、付けなかった場合に MARS がどう補うか)ですが、常識的に決められる場合が多いものの、’94 標準はルールとして定義しています。
DAT, NOP
- 常に
.F だが無視される。
MOV, SEQ, SNE, CMP
- A-mode が即値なら
.AB、
B-mode が即値で A-mode が即値でないなら .B、
どちらも即値でないなら .I。
ADD, SUB, MUL, DIV, MOD
- A-mode が即値なら
.AB、
B-mode が即値で A-mode が即値でないなら .B、
どちらも即値でないなら .F。
SLT, LDP, STP
- A-mode が即値なら
.AB、
即値でないなら(常に).B。
JMP, JMZ, JMN, DJN, SPL
- 常に
.B(ただし JMP と SPL では無視される)。
’94 標準における即値アドレッシング(#)の定義は、かなり独特です。旧来の構文と 100% 互換でありながら、即値は非常に巧妙で一意な方法で定義されており、あらゆる命令と修飾子と論理的に組み合わせられ、強力な道具になります。
修飾子を見て、MOV.F #7, 10 が何をするのか疑問に思うかもしれません。.F は両フィールドを移すはずなのに、ソースには数値が 1 個しかありません。では 7 をデスティネーションの両フィールドに入れるのでしょうか?
いいえ、そうはなりません。実際には、デスティネーションの A-field に 7 を、B-field に 10 を移します。なぜでしょう?
理由は、’94 の構文では、ソース(およびデスティネーション)は 常に 命令全体だからです。即値の場合、ソース命令は「現在の命令(すなわち 0)」として扱われます。実際の値が何であってもそうです。したがって MOV.F #7, 10 は、ソース(0)の両フィールドをデスティネーション(10)へ移すのです。驚きますよね?
同じ考え方は MOV.I にも適用されます。この定義により、修飾子なしでは ’88 標準で意味を持たなかった JMP #1234 のような命令も合理的に使えます。数値へジャンプはできませんが、「その数値のアドレス(0)へジャンプ」なら可能です。これは多くの利点をもたらします。A-field に「ただで」データを置けるだけでなく、誰かにデクリメントされてもコードが生き残ります。先ほどの Imp 生成コードも少し丈夫にできます:
SPL #0, }1
MOV.I #1234, 1
動きは同じですが、今度は A-field が自由になりました。お遊びとして、SPL に Imp の A-field をインクリメントさせ、Imp がそれぞれ違う見た目になるようにしています。SPL は B-field を使わないので、この増分も「ただ」です。動作します。信じてもよいし、自分で試してもよいでしょう!
core のアドレスが環状であることは既に分かっているはずです。現在命令から coresize だけ進んだり戻ったりすると、同じ命令を指します。しかし実際には、これはさらに深いところまで及びます。Core War では全ての数値が 0 から coresize-1 の範囲に正規化されます。
範囲付き整数演算に慣れている人向けに言うと、Core War の数値は符号なしとして扱われ、最大値は coresize-1 です。それでもピンと来なければ続きを読んでください。
要するに、Core War の全ての数値は core の長さ coresize で割られ、余りだけが保持されます。たとえば 8 桁しか表示できない電卓を想像してください。桁あふれの上位桁は捨てられます。100*12345678 は 1234567800 ですが 34567800 だけが表示され保持されます。同様に core=8000 の場合、7900+222 は 8122 ですが 122 になります。
負数はどうなるでしょう? これも正規化されます。正になるまで coresize を足します。つまり -1 は MARS では coresize-1 として保存され、core=8000 なら 7999 です。
アドレスは元から環状なので、これは大抵は問題になりません。単純な ADD や SUB でも、coresize=8000 なら 6+7998 は 4(または 8004)になり、6-2 と同じ結果です。
では何が問題かというと、いくつかの命令では差が出ます。DIV、MOD、SLT は常に符号なしとして扱います。したがって -2/2 は -1 ではなく、(coresize-2)/2 です。つまり (coresize/2)-1(core=8000 なら 7998/2=3999)になります。同様に SLT は -2(7998)を 0 より 大きい とみなします。Core War では 0 が最小であり、それ以外は全て 0 より大きいのです。
ここまで耐えてくれたあなたにご褒美です。これまでは断片的な情報を渡してきました。ここで一度まとめとして、各命令を順に説明します。
もちろん、最初に命令一覧を出した時点で説明しておけば推測が減ったでしょう。でも待った理由があります。実用的なコードを先に見せたかったのと、何よりアドレッシングモードと修飾子の基本感覚を掴んでから命令を説明したかったからです。修飾子の前に命令を説明すると、まず ’88 のルールを教え、後で修飾子込みでもう一度教える必要が出ます。学習法として悪くないですが、このガイドが不必要に複雑になります。
DAT
- 元々
DAT は名前通りデータ保存用でした。Core War では命令数を減らすため、未使用フィールドにポインタなどを入れることが一般的です。そのため DAT の最重要点は「実行するとプロセスが死ぬ」ことです。’94 標準では不正命令が存在しないため、DAT は完全に合法な命令として定義され、現在実行中のプロセスをプロセスキューから取り除きます。些細な言い回しに見えるかもしれませんが、当たり前を厳密に定義すると混乱が減ります。
修飾子は DAT に影響しません。MARS によっては削除します。ただし、プリデクリメント/ポストインクリメントは値が使われなくても評価されることを忘れないでください。もう 1 つ、過去標準の名残として、引数が 1 つしかない場合は B-field に入ります。
MOV
MOV は命令から命令へデータをコピーします。ここまでの章で十分理解できていなければ、前を読み返した方がよいでしょう。MOV は数少ない .I 対応命令で、修飾子が省略され(かつ即値が絡まないなら)デフォルトは .I
ADD
ADD はソース値をデスティネーション値へ加算します。修飾子の挙動は MOV に似ていますが、.I はサポートされず .F と同じ意味になります。(MOV.AB と DJN.F の足し算、という概念は意味を持たないでしょう?) また、Core War の演算は全て modulo coresize で行われます。
SUB
- これも
ADD と同様に動きますが、違いは明白です。実際、算術系命令はだいたい同じです。
MUL
- …
MUL も同様です。何をするか分からないなら、かなり重要なものを見落としています。
DIV
DIV も概ね同様ですが、注意点があります。まず 符号なし除算 なので意外な結果になることがあります。ゼロ除算はプロセスを殺します(DAT と同様)かつデスティネーションは変更されません。DIV.F や .X で 2 つ同時に割り算をして、一方の除数が 0 でも、もう一方の割り算は通常通り行われます。
MOD
DIV と同様の注意が全て当てはまります(ゼロ除算の扱いも含む)。MOD.AB #10, #-1 のような結果は core サイズに依存することを覚えてください。一般的な 8000 core なら結果は 9(7999 mod 10)です。
JMP
JMP は A-field が指すアドレスへ実行位置を移します。算術命令との重要な違いは、JMP は「そのアドレスが指すデータ」ではなく「アドレスそのもの」だけを気にする点です。さらに JMP は B-field を使わないため、修飾子も無視します。2 つのアドレスへ同時にジャンプ(あるいは分岐)できると強すぎますし、次の 3 命令の実装も難しくなります。とはいえ、未使用の B-field に増減を仕込めば、運が良ければ相手コードを傷つけられます。
JMZ
JMZ は JMP に似ていますが、B-field を無視せず、B-field が指す値をテストし、0 ならジャンプします。0 でないなら次へ進みます。テスト対象が 1 命令だけなので修飾子の選択肢は限られます。.AB は .B と同義、.BA は .A と同義、.X と .I は .F と同義です。JMZ.F で両フィールドをテストする場合、両方が 0 のときだけ ジャンプします。
JMN
JMN は JMZ と同様ですが、テスト値が 0 でない 場合にジャンプします(驚きはありませんね)。JMN.F は どちらかのフィールドが 0 でなければ ジャンプします。
DJN
DJN は JMN に似ていますが、テストの 前 に値を 1 減らします。ループカウンタに便利ですし、相手の破壊にも使えます。
SPL
- これが大物です。
SPL の導入は Redcode 史上最も重要な変更の 1 つで、ICWS ’94 の導入と並ぶほどです。SPL は JMP のように動きますが、実行は 次の命令でも 継続し、プロセスが 2 つに分かれます。次の命令側のプロセスが、ジャンプ先へ行くプロセスより 先に 実行されます。これは小さな違いに見えますが、非常に 重要です(多くの現代ウォリアーがこれなしでは動きません)。最大プロセス数に達している場合、SPL は NOP と同じです。JMP と同様に B-field と修飾子は無視されます。
SEQ
SEQ は 2 命令を比較し、等しければ次の命令をスキップします。(飛ぶのは 2 命令先だけです。ジャンプ先を書ける場所がありません。) 等値比較なので .I がサポートされます。.F、.X、.I では、全フィールドが一致 したときだけスキップします。
SNE
- 予想通り、比較した命令が等しくない場合に次命令をスキップします。複数フィールドを比較する場合、どれか 1 組でも一致しなければスキップします。(
JMZ と JMN の関係と似ていますね。)
CMP
CMP は SEQ の別名です。SEQ/SNE が導入される前はこの名前しかありませんでした。今では多くの MARS が ’88 モードでも SEQ を認識するので、どちらを使っても大差ありません。
SLT
SLT は「第 1 の値が第 2 より小さい」場合に次命令をスキップします。これは論理比較ではなく算術比較なので .I を使う意味はありません。SGT(大きいならスキップ)があってもよさそうですが、多くの場合はオペランドを入れ替えれば同じ効果が得られます。全ての値は符号なし として扱われるため、0 が最小で、-1 が最大 です。
NOP
- 何もしない命令です。(何もしないことに関しては完璧です。) 実戦ウォリアーではほぼ使いませんが、デバッグには有用です。増減が設定されていれば、それは評価されます。
なお LDP と STP の 2 命令が未説明ですが、比較的新しい追加であり、次で説明します。:-)
P-space は pMARS 0.8 で導入された Redcode の最新要素です。P は private / permanent / personal / pathetic など、お好きな意味でどうぞ。要するに P-space は「自分のプログラムだけがアクセスでき」「マルチラウンドの対戦でラウンド間を越えて保持される」メモリ領域です。
P-space は通常の core と多くの点で異なります。まず、各 P-space の場所は命令ではなく数値 1 つだけを保存します。また、P-space のアドレスは絶対で、core 内のどこにその命令が置かれていても P-space のアドレス 1 は常に 1 です。さらに、P-space へアクセスできる命令は LDP と STP の 2 つだけです。
この 2 命令の構文は少し変わっています。たとえば STP は core の通常値をソースに取り、その値を「デスティネーションが指す P-space フィールド」に書き込みます。つまり P-space の場所は「デスティネーションの アドレス」ではなく「デスティネーションの 値」で決まります。もしこれが MOV だったなら上書きされる値、ということです。
たとえば STP.AB #4, #5 は、値 4 を P-space フィールド 5 に保存します。同様に、
STP.B 2, 3
...
DAT #0, #10
DAT #0, #7
は、値 10 を P-space フィールド 7 に書き込みます。3 ではありません! STP 自身が間接アドレッシングを使うと、ある種の「二重間接」になり、かなり混乱します。
LDP は同様ですが、ソースが P-space フィールドになり、デスティネーションが core 命令になります。
P-space のサイズは通常 core より小さく、典型的には core サイズの 1/16 です。P-space のアドレスも core と同様に環状です。P-space のサイズは core サイズの因数でなければならず、そうでないと妙なことが起きます。
P-space の 0 番地は特別で、各ラウンドの前に特別な値で初期化されます。第 1 ラウンドでは -1、前ラウンドで死んだ場合は 0、それ以外は生存プログラム数です。1 対 1 なら 0 は負け、1 は勝ち、2 は引き分けです。この初期化により、0 番地はラウンド間の任意情報の受け渡しには使えません。次ラウンド開始前に MARS が上書きするからです。
pMARS 実装には P-space に関する癖があります。P-space アクセスを遅く保つ意図から、1 命令で 2 フィールド同時にロード/セーブすることは許可されていません。これは良いのですが、結果は少なくとも不格好です。具体的には LDP.F、.X、.I は全て LDP.B と同じ動きになります(STP も同様です)。
P-space の最も一般的な用途は、戦略選択です。最も単純には、前回の戦略を P-space に保存し、P-space の 0 番地が「前回負け」を示していれば戦略を切り替えます。この種のプログラムは P-warriors、P-switchers、P-brains(pea-brains と発音)などと呼ばれます。
残念ながら P-space は見かけほど私的ではありません。相手はあなたの P-space を直接読み書きできませんが、あなたのプロセスが捕獲されて相手コード(STP を含む)を実行させられることがあります。この種の手法は brainwashing(洗脳)と呼ばれ、P-switcher はそれに備え、戦略フィールドが妙な値になっても暴走しないようにする必要があります。
これまでは、例のプログラムでアドレスを「現在命令からの相対命令番号」で書いてきました。しかし大きなプログラムでは面倒で、読みづらくもなります。幸い、Redcode ではラベル、記号定数、マクロなど、良いアセンブラに期待する機能が使えます。命令にラベルを付け、ラベルで参照すれば、実アドレスはパーサが計算してくれます:
imp: mov.i imp, imp+1
どうでしょう? これは最初に出したプログラムと同じです。数値アドレスをラベル "imp" 参照に置き換えただけです。ただ、この例では有効性は薄いです。ラベルを使っているのは "imp" 自身だけで、ここは 0 に置き換わるからです。
実行前に、MARS のパーサはラベルなどの記号を数値へ変換します。こうして「前処理された」Redcode ファイルは load file(ロードファイル)と呼ばれます。全ての MARS はロードファイルを読めますが、完全なパーサを持たないものもあります。ロードファイル形式では先ほどのコードは MOV.I 0, 1 になります。また、同じコードを次のようにも書けます:
imp: mov.i imp, next
next: dat 0, 0
この場合、"next" は "imp" の 1 命令後なので 1 に置き換わります。実アドレスは相対数値のままなので、Imp が "next" の上へコピーして進んでも、Imp は相変わらず MOV.I 0, 1 のままです。
ラベル末尾の : は必須ではありません。ここでは見やすさのため付けましたが、私は自分のコードでは通常付けません。好みの問題です。
念のため。Redcode 命令は大文字小文字を区別しません。私はソースは小文字の方が見た目が良いので小文字を好み、コンパイル済み(ロードファイル)だけ大文字にします(主に伝統です)。
これまでの例はコンパイルできるかもしれませんが、プログラムの断片に過ぎません。典型的な Redcode ファイルには、MARS 向けの追加情報があります。
;redcode-94
;name Imp
;author A.K. Dewdney
org imp
imp: mov.i imp, imp+1
end
既に気づいていると思いますが、Redcode では 以降はコメントです。ただし先頭の数行は単なるコメントではありません。MARS はそこからプログラム情報を取得します。
最初の行 ;redcode-94 は「これは Redcode ファイルである」ことを示します。この行より上は MARS に無視されます。実際は ;redcode で始まればよいのですが、残りで Redcode の方言を識別できます。特に KotH サーバ はこの行を自分で読み、どの hill に送るかを判断します。
;name と ;author はプログラム情報です。任意形式でも書けますが、特定コードを使うと MARS が読み取って実行時に表示できます。
END 行は、(予想通り)プログラムの終端です。以後は無視されます。;redcode と組み合わせると、メールに Redcode を埋め込む用途にも使えます。
ORG は実行開始位置を示します。これにより、プログラム本体の前に他命令を置けます。ORG は ’94 標準で追加されたものです。古い書き方では開始アドレスを END の引数にします。
;redcode-94
;name Imp
;author A.K. Dewdney
imp: mov.i imp, imp+1
end imp
単純で短いのですが、残念ながら理屈としては微妙です。長いプログラムでは開始位置を見るために末尾までスクロールする必要も出ます。Redcode では ORG も END も pseudo-OpCodes(疑似命令)と呼びます。命令のように見えますが、実際にはプログラムへコンパイルされません。
Imp はこのくらいにして、Dwarf を現代 Redcode で書くとこうです:
;redcode-94
;name Dwarf
;author A.K. Dewdney
;strategy 一定間隔で core を爆撃する。
;assert CORESIZE % 4 == 0
org loop
loop: add.ab #4, bomb
mov.i bomb, @bomb
jmp loop
bomb: dat #0, #0
end
ラベルがあると理解しやすいですね。ここではコメント行も 2 つ追加しました。;strategy はプログラムの簡単な説明で、複数行書けます。多くの MARS は無視しますが、hill は ;strategy を表示します。上のプログラムを送ると、例えば次のように見えるかもしれません:
A new challenger has appeared on the '94 hill!
Dwarf by A.K. Dewdney: (length 4)
;strategy Bombs the core at regular intervals.
[other info here...]
例に新しく出てきたのが ;assert 行です。これはプログラムが現在の設定で本当に動作するかを確認するために使えます。Dwarf は core サイズが 4 で割り切れないと自滅するので、;assert CORESIZE % 4 == 0 を入れて常に成り立つことを保証しています。
CORESIZE は core サイズを示す定義済み定数です。つまり n+CORESIZE は常に n と同じアドレスです。% は剰余演算子です。;assert 行などで使える式の構文は C 言語と同じ ですが、演算子の種類はかなり限定されています。
C を知らない人向けに、Redcode 式で使われる演算子の一覧です:
- 算術:
+ 加算
- 減算(または符号反転)
* 乗算
/ 除算
% 剰余
- 比較:
== 等しい
!= 等しくない
< 小さい
> 大きい
<= 以下
>= 以上
- 論理:
&& AND
|| OR
! NOT
- 代入:
= 変数 への代入
;assert の後ろには論理式が続きます。偽ならコンパイルされません。C と同様、0 が偽でそれ以外が真です。論理演算子と比較演算子は真なら 1 を返し、後で便利なことがあります。
典型的には ;assert は core サイズが想定通りか確認します(例:;assert CORESIZE == 8000)。P-space を使う場合は ;assert PSPACESIZE > 0 で存在確認できます。Dwarf は適応性があるので、特定値ではなく割り切れ条件だけを確認しました。Imp は どんな 設定でも動くので ;assert 1 や ;assert 0 == 0 のように常に真の式でも構いません。そうしないと MARS が "missing ;assert line -- warrior may not work with current settings." と警告する場合があります。
定義済み定数の一部(CORESIZE など)は ’94 標準が定義し、他は追加され得ます。pMARS 0.8 は少なくとも次をサポートするはずです:
- CORESIZE — core のサイズ(既定 8000)
- PSPACESIZE — P-space のサイズ(既定 500)
- MAXCYCLES — 引き分けになるまでのサイクル数(既定 80000)
- MAXPROCESSES — プロセスキュー最大(既定 8000)
- WARRIORS — core 内のプログラム数(通常 2)
- MAXLENGTH — プログラム最大長(既定 100)
- CURLINE — いままでにコンパイルした命令数(0 から MAXLENGTH)
- MINDISTANCE — 2 ウォリアー間の最小距離(既定 100)
- VERSION — pMARS バージョン×100(80 以上)
定義済み定数もラベルも便利ですが、それだけでしょうか? 変数のようなものは使えないのでしょうか?
Redcode はアセンブリ言語なので変数は多く使いません。しかしそれに近いもの(時にはそれ以上)があります。疑似命令 EQU で独自の定数、式、さらにはマクロ的なものも定義できます:
step equ 2667
これ以後、step は常に 2667 に置換されます。ただし罠があります。置換は数値ではなくテキストです。この例なら問題になりにくいですが、EQU を強力にする一方で、C のマクロと同様の問題も起こります。例を見ましょう。
step equ 2667
target equ step-100
start mov.i target, step-target
MOV の A-field は 2567 になりますが、B-field は 2667-2667-100 == -100 になってしまいます。本来は 2667-(2667-100) == 100 を意図したでしょう。解決は簡単です。EQU の式には常に括弧を付けることです。例えば "target equ (step-100)" のように。
最近の pMARS では複数行 equ が使え、マクロ風にできます:
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
dec7
pMARS パーサの機能はまだあります。これは上のどれよりも強力で(そして学びにくい)かもしれません。FOR/ROF はソースを短くし、複雑なコード列を簡単に作るだけでなく、設定に応じた条件付きコードも作れます。
FOR ブロックは疑似命令 FOR と「繰り返し回数」で始まります。前にラベルがあると、ループカウンタとして使われます:
index for 7
dat index, 10-index
rof
ブロックは ROF で終わります。(古い決まり文句 NEXT や REPEAT より良いと思います。) 上は pMARS により次のように展開されます:
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
FOR ブロックは入れ子にもできます。中に EQU も入れられ、面白いコードが作れます。さらに便利なのは、ループカウンタを & 演算子でラベルに結合できる点です。ラベル重複を避ける用途が多いですが、他にも使えます。
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
これは FOR/ROF の処理後、次になります:
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
これを何に使うかは想像次第です。ここまで複雑な式を使うウォリアーは、私が見た範囲では quickscanner くらいです。定義済み定数は FOR/ROF でも使えます。例えば:
decoy
foo for (MAXLENGTH-CURLINE)
dat 1, 1
rof
end
これはウォリアーの残り領域を DAT 1, 1 で埋めます。自分のプログラムをデコイから離れた場所へコピー(boot)していれば、攻撃の誤誘導に使えます。ここで foo を意味のないループカウンタとして置いているのは、そうしないと MARS が decoy をループカウンタと誤認するためです。
最後に、FOR/ROF をより創造的に使う例です:
;redcode-94
;name Tricky
;author Ilmari Karonen
;strategy とても複雑なウォリアー的な何か
;strategy (条件付きコードの分かりやすい例)
;assert CORESIZE == 8000 || CORESIZE == 800
;assert MAXPROCESSES >= 256 && MAXPROCESSES < 10000
;assert MAXLENGTH >= 100
org start
for 0
rof
for (CORESIZE == 8000)
step equ normalstep
rof
for (CORESIZE == 800)
step equ tinystep
rof
for 0
;strategy strategy と assert 行はコメントなので、FOR 0 / ROF の中でも解析されます!
rof
EQU で定義した定数の問題は、まあ、定数であることです。定義後に値を変えられません。多くの場合はそれで十分ですが、いくつかの技巧がほぼ不可能になります。
幸い pMARS には実変数が少し用意されています。使い方は少し癖があり、実際に使う人は長らく見ていませんが、存在はします。
変数名は 1 文字で、実質 26 個(a から z)です。EQU ではなく、= で代入します。癖のある点は、代入するには式が必要なことです。さらに pMARS はコンマ演算子を認識しないので、ダミー式が必要になる場合があります。
それでも変数は役に立ちます。例えば次の自動生成フィボナッチ列は、変数がなければおそらく不可能でしょう。
dat #1, (g=0)+(f=1)
idx for 15
dat #idx+1, (f=f+g)+((g=f-g) && 0)
rof
(g=f-g) を 0 との AND で「隠して」いる点に注目してください。この仕組みは、pMARS が式を並べ替えず、加算の左側を常に先に評価するため成立します。右側を計算するときには f が既に増えている、というわけです。
さて、もう 1 つ疑似命令がありました。ほとんど使われませんが存在します。PIN は "P-space Identification Number" の略です。2 つのプログラムが同じ PIN を持つと P-space を共有します。これはプロセス間通信や協調にも使えます。しかし、効果的で高速な通信手段を作る手間に見合う戦略は、あまりなさそうです。とはいえ試したいならどうぞ。成功するかもしれません。
プログラムに PIN がない場合、P-space は常に私有です。共有していても、特殊な 0 番地(読み取り専用として扱われる特別初期化領域)は常に私有です。
もし知らないなら説明します。King of the Hill サーバ(しばしば hills と呼ぶ)は、インターネット上で継続運用される Core War トーナメントです。ウォリアーをメール(または Web フォーム)で送ると、サーバが hill 上の(通常 10〜30)プログラムすべてと対戦させます。合計スコアが最低のプログラムは hill から落ち、新規ウォリアーがそれに入れ替わります(少なくとも 1 つ以上の既存プログラムより良いスコアを出した場合)。この基本形には、無限 hill、diversity hill など様々な派生もあります。
hill は実行時間節約のため、実際の対戦前にロードファイルへ事前コンパイルすることがよくあります。その結果、WARRIORS のような定義済み定数が不正確になり、謎の ;assert 問題を引き起こすことがあります。
現在(2012 年 4 月時点)利用可能な主な KotH サーバは 2 つあります:
- KotH.org
- 現存する中で最古かつ最も有名な KotH サーバ。現在 7 つの hill をホストし、設定の違いがある。うち 2 つはマルチウォリアーの melee、2 つは古い Redcode ’88 標準を使う。
- KOTH@SAL
- こちらも 7 つの hill をホスト。Beginners' hill(初心者 hill)があり、50 回のチャレンジを生き残ると自動的に押し出され、新規プレイヤーが載りやすいようになっている。
また、Koenigstuhl サーバは、公開済みウォリアー向けに 10 個の「無限」hill をホストしています。これらの hill ではウォリアーは押し出されず、hill は増え続けます。Koenigstuhl はランキングに応じて寄与度を調整する再帰的スコアリングアルゴリズムも使っています。
上の一覧は網羅ではなく、古くなる可能性があります。稼働中の KotH サーバ一覧のより詳しく最新の情報は、(現時点では)corewar.info のページ にあります。
- v. 0.50
- パーサ章を書き終えた。(1997 年 3 月 25 日)
- v. 0.51
- for/rof 例のバグを修正
- v. 0.52
- 最初の公開版
- v. 0.53
- 誤字脱字を修正
- v. 0.54
- ’88 から ’94 への変換ルールを追加
- v. 0.55
- HTML を少し整形
- v. 1.00
= 演算子の情報を追加。これを機に「バージョン 1」とする。(1997 年 5 月 5 日)
- v. 1.01
<DD> タグの小さな互換性問題を修正。
- v. 1.02
- 誤字と不自然な文を修正。ナビゲーションバーをサイトの他ページと共通スタイルに変更。
- v. 1.03
- 画像と align 属性を削除し、doctype を Strict に変更。
- v. 1.10
- うわっ! ずっと
SLT の説明を逆に書いていた!修正。(1998 年 3 月 8 日)
- v. 1.20
- Climbing the hill を現状に合わせて大幅に書き直し、他にも軽微な変更。文書を vyznev.net の新アドレスへ移動。Creative Commons ライセンスへ変更。(2003 年 10 月 7 日)
- v. 1.21
- CSS 対応ブラウザ向けに色を追加。軽微な変更と誤字修正。タイトルの表記を統一。vyznev.net での最初の公開版。(2004 年 4 月 11 日)
- v. 1.22
- ライセンスを CC-By-NC 2.0 から CC-By 3.0 に更新し、商用利用制限を撤廃。ページ移動通知ボックスを削除。KotH サーバ一覧を更新し、停止した hill を削除。内容はそれ以外変更なし。(2012 年 4 月 16 日)
- v. 1.23
- pMARS における P-space の 0 番地の扱い説明の長年の誤りを修正。(読み取り専用ではなく、各ラウンド前に自動上書きされる。) Aritz Erkiaga が 発見 し報告してくれたことに感謝。(2020 年 8 月 11 日)
Copyright 1997-2020 Ilmari Karonen.
本作品は Creative Commons Attribution 3.0 Unported
License(CC BY 3.0)で提供されています。
この日本語版は上記原文を翻訳した派生物です。原著者・出典・ライセンス表記を保持し、変更(翻訳)を明示しています。