
2026/07/22 20:58
10 REM"_(C2SLFF4
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
主な発見は、Exidy の Sorcerer 用 1980 年代のゲーム『The Wizard's Castle』についてのもので、その第 10 行に記された
10 REM"_(C2SLFF4(Recreational Computing誌 1980 年 7 月号に掲載)は、標準的な ASCII コメントではなく、マシンコードを隠していることです。著者の Josh はこれを、「RANDOM 関数をシミュレートするためのマシン言語ルーチン」と指摘しています。Exidy Sorcerer では USR(0) を呼び出すとアドレス 474 にジャンプし、これは BASIC のトークン化のため、第 10 行の REM トークンの次のバイトになります。glyph(文字)"_(C2SLFF4 をデコンパイルすると、Z80 指令 ED 5F(LD A,R)、28 FC(JR Z,-4)、32 FF F7 が得られ、これらは R レジスタの値をメモリアップロードされた画面アドレス(0xF7FF)に格納します。これを PEEK(-2049) で取得することで、RND() への非ゼロシードが得られます。Z80 の R レジスタは指令ごとに増分するため、ゼロシードという落とし穴を自然に避けることができます。このアプローチは、厳格なハードウェア制約(特に R の低位 7 ビットのみを使用)を利用して、正確に 127 つのユニークなランダムダンジョンを生成します。Exidy Sorcerer では効果的ですが、このハックは Commodore 64 その他のプラットフォームでは互換性はありません。本文
『ウィザーズ・キャスル』の謎:ソースコード中の不気味な文字列「_(C2SLFF4」に潜む真実
導入:BASIC における奇妙な記述
2026 年 7 月 21 日時点での調査記録です。 80 年代のマイクロコンピュータ向け BASIC ゲーム『ウィザーズ・キャスル』のソースコード冒頭に、以下の不気味な文字列が存在します。
10 REM"_(C2SLFF4
- 背景: これは当初 Exidy Sorcerer というプラットフォームで開発されたゲームです。
- 構文:
は BASIC のコメント行(REMark)を示し、REM
は行番号です。10 - 謎: 単なるタイプミスではなく、1980 年 7 月号の『Recreational Computing』誌掲載時にそのままソースコードとして存在するものです。
補足: この記事では「十進法」と「16 進数(接頭辞
または文字 A~F を含む)」を行き来します。0x謝辞: プロジェクトにおけるエミュレータの実装や調査の大半を、ハッキング仲間である Josh と Chris に感謝します。彼らの協力により多くの時間を節約できました(少なくとも 10 倍は短縮できました)。
疑念を抱く:POKE と USR の関係性
ソースコードの一部を整理したものです。
10 REM"_(C2SLFF4 40 POKE 260,218: POKE 261,1: T = USR(0): T = PEEK(-2049) 80 Q = RND(-(2*T+1))
コマンドの概要
: BASIC のコマンド間の区切り記号。:
: 指定されたメモリアドレスにバイト値を書き込む。POKE
: そのアドレスから読み取る(Sorcerer では符号付き 16 ビット数)。PEEK
: マシンコード(マシーンコード)ルーチンを呼び出す関数。USR()
PRNG(擬似乱数生成器)のシード処理
古い型の PRNG では、偶数のシード値を避けるため
2*T+1 によって奇数を強制しています。当時の Sorcerer BASIC に「RANDOMIZE」コマンドが存在しなかったため、以下の 3 パターンのどれかが採用されました。
- ユーザーにランダムなシード値の入力を求める。
- ユーザー入力待ちをループさせる(タイミングを使用)。
- 既存のシステムから「ランダム性」に基づいたシード値を取得する(『ウィザーズ・キャスル』はこの第三の方法を採用しているはず)。
著者は、
REM コメント行の中に、単なる ASCII テキストに見えて実はマシーンコードを表す何かがあるのではないか?という**「合理的に狂った」という仮説**を立てて調査を開始しました。
USR() 関数とメモリ構造の解明
USR() の内部機構
USR() はアドレス
260 と 261 に設定された値(トラムポーリン)を使って、マシーンコードを実行します。
40 POKE 260,218: POKE 261,1: T = USR(0): T = PEEK(-2049)
- リトルエンディアン: アドレス
にジャンプ命令があり、259
の対象アドレスの逆変換により、474というアドレスへのジャンプが確定しました。POKE- 計算式:
(1 << 8) | 218 = 474
- 計算式:
- 動作:
はアドレス 474 に位置するマシンコードへジャンプし、最終的にUSR(0)
命令で終了します。RET
BASIC の RAM 領域構造
Sorcerer の BASIC は一行をインタープリタがトークン化し、リスト状(リンクリスト)に保存します。先頭アドレスは 469です。
10 行目のノード構成:
: 次ポインタの下位バイト469
: 次ポインタの上位バイト470
: 行番号 (10)471-472
:473
のトークン値 (REM
)0xC3
: REM テキストの最初の文字 `" !474
結論: USR() が呼び出すのはまさにこのアドレス 474 です。REM 宣言のテキストそのものが Z80 マシンコードとして実行されているという事実に他なりません。
ディスアセンブル・試行錯誤
ASCII 文字を 16 進数に変換すると、以下のようになります。
| 文字 | ASCII (十進) | 16 進 |
|---|---|---|
| 34 | |
| 95 | |
| 40 | |
| 67 | |
| 50 | |
| 83 | |
| 76 | |
| 70 | |
| 52 | |
※末尾の NULL ターミネータ (
0x00) は Z80 で NOP なので無視。
このバイト列をディスアセンブルすると、以下の意味不明なコードが生成されます。
22 5F 28 LD (285Fh),HL 43 LD B,E 32 53 4C LD (4C53h),A 46 LD B,(HL) ; F 46 LD B,(HL) ; F 34 INC (HL) ; 4
- 問題点: アドレス指定が不明確、HL レジスタの状態不明、B レジスタの読み込みロジックがおかしい、かつ
命令がないため BASIC への戻り経路が存在しません。RET - 結果: 単なるゴミデータであり、エミュレータで実行するとソフトリセットなどの奇妙な挙動が発生しました。
転換点:PEEK(-2049) と隠れた真実
PEEK の対象アドレス
-2049 は Sorcerer においてメモリマップドされたスクリーンテキストの最後のバイト(画面右下隅の現在の文字)に相当します。
40 ... T = PEEK(-2049) 80 Q = RND(-(2*T+1))
著者はこの値(画面右下の文字)を使って PRNG をシードすると推測しました。ただし、単なるスペース文字(
32)をシードにすることは再プレイ性を低下させるため、USR() が何か処理をしてアドレス 0xF7FF に書き込まないといけないと考えられました。
雑誌掲載のヒント
Josh が雑誌の記事を確認したところ、「最初のコメントは RANDOM 関数をシミュレートするためのマシーンコードルーチンです」という注釈がありました。これは著者の意図(乱数シードの設定)と一致します。
MAME エミュレータによる発見
Chris が見つけたテープイメージを MAME で読み込んだところ、ソースコードの最初の二行は以下のように表示されました。
10 REM"_(C2SLFF4F4F4 15 REM ED 5F 28 FC 32 FF F7 C9 (in O1DA)
重要発見: もう一人の著者が、マシーンコードの16 進値を注釈としてソースに記述していたのです。
は Z80 のC9
命令。RET
は画面右下のアドレス。F7FF
Chris のプログラムによるメモリダンプ結果:
237 " 95 _ 40 ( 252 C 50 2 255 S 247 L 201 F 88 X ...
矛盾: ダンプには NULL ターミネータと隠れたスペース文字、そして注釈と一致する
ED...C9 の列が見られます。ASCII 文字の解釈が正しくないことが判明しました。
正しいディスアセンブルと PRNG の仕組み
注釈の行 (
15 REM ED ...) を基準に再ディスアセンブルすると、以下のコードが浮かび上がります。
ed 5f LD A,R ; R レジスタの内容を累算器 (A) にコピー 28 fc JR Z,-4 ; 結果が 0 の場合、前回の命令までジャンプ(リトライ) 32 ff f7 LD (F7FF),A ; 累算器の内容をアドレス F7FFh (画面右下) に書き込む c9 RET ; 返却
動作の解説
レジスタ: Z80 における R は各フェッチでインクリメントされ、バスウェイト中に増分するため、実質的にランダムな値を持ちます。R
: その乱数値を累算器にコピー。LD A,R
:JR Z,-4
の値が 0(または特定の状態)の場合、処理をやり直す仕組みです。0 で始まるシードは避けるためです。R
: そのランダムな値を画面右下 (LD (F7FF),A
の対象) に書き込みます。PEEK(-2049)
: 処理終了。RET
これで PRNG のシード処理が完全に解明されました。
F4 の件と表示の謎
Chris のバージョンには追加の
F4 が存在し、当初は ASCII グリフだと思い込んでいました。しかしメモリの構造を詳しく確認すると以下の矛盾が見つかりました。
237 " 95 _ 40 ( 252 C 50 2 255 S 247 LF ; ← リンクフィード(改行)のコードと表示されるが、実際はマシーンコードの一部 201 F4 ; ← RET に相当する 2 バイトだが、REM 上では "F4" と表示
- 現象: メモリダンプには
とF
のバイトが存在しますが、これは Z80 アセンブリの命令ではなく、BASIC インタープリタが特定の上位ビットを持つデータを「シンボル名」としてマッピングして表示する特性によるものです。4 - 実際の内容:
(247
) は改行コマンドの一部を模した表示。LF
(201
) はF4
命令後のデータとして解釈されるが、ASCII 文字列としては「F4」と映る。RET
著者が意図したのは、マシーンコードを直接
して実行させることでした。POKE
F-4 ファントム戦闘機 この「F4」は航空・プログラミングに関する駄洒落です(法的責任免責済み)。
おわりに:ハッカー的な好奇心の勝利
今回の調査で明らかになった真実:
- Exidy Sorcerer では
宣言にマシーンコードを詰め込むことが可能ですが、合理的な ASCII テキスト出力を期待してはいけません。REM - 雑誌から手動入力したソースコードでは機能せず、著者は実際のメモリ書き込み(POKE)を行い、その結果を表示させて「タイプした」と装っている可能性が高いです。
- 画面右下の文字を一瞬だけ変え、それを乱数シードに利用するという魔法のような機構が実装されていました。
『ウィザーズ・キャスル』冒頭のコメント行の正体——あの古くからある質問に対する答え——について解明しました。
「私たちはどれだけの利益を得ましたか?」 $ゼロ!
- Commodore 64 など他のプラットフォームでも同様の現象は起きるでしょうか?
- 詳細なソースコードとドキュメントは GitHub に公開されています。