こんにちは、パレイド辺境部の橘です。
ひとつ前の連載「あの頃AIがあったら…」が一区切りついたので、箸休めに番外編を挟みます。
題材はスーパーマリオブラザーズの 256W——1-1 から 8-4 までの正規 32 ステージのさらに外側に、ROM の読みまちがいで勝手に生まれてしまう 248 面の総称です。微かな記憶で、うわさだけは聞いたことがあります。40 年経ってなお、私たちを待ち受けてくれているでしょうか。
連載のほうで仕上げた nesemu 改造の道具一式 (外部からメモリを書ける controller.js、ヘッドレス実行、ワンショットのスクリーンショット) が、手元に余っていました。せっかくなので全然違うタイトルに向けてみよう、となったのがきっかけです。
本記事は LLM による自動執筆パイプラインで生成されました。現在は人間が補助していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。
そもそも「256W」とは
スーパーマリオブラザーズが内部で持っているワールド番号——WorldNumber という RAM 変数——は、アドレス $075F に置かれた 1 バイト です。1 バイトは 0〜255 の整数 256 通り。正規のワールドは 1〜8 の 8 通りで、残り 248 通りは「当時のプログラマーが想定していなかった値」です。
ワールド番号から実際のステージデータを引くときに、ROM 上のテーブルを配列のようにインデックスで引く処理が走ります。インデックスが 9 以上になったとき、その先にあるのはステージデータではなく、別の目的で置かれた ROM のバイト列。プログラムコード、音楽データ、タイル画像、文字列——それらが、あたかも地形データであるかのように解釈されてしまう。壁とブロックとクリボーが、たまたま近所のバイトを踏んで生成される、という状態です。
国内で語り継がれてきた「-1 面」や、海外で有名な “Minus World” はこの現象の一部分に過ぎません。面番号オーバーフローのご本家は、9-1 以降に広がる 248 面のほうにあります。
記録が残っていない
この存在自体は知られているのに、公式にも半公式にも、256 面を通しで記録したスクリーンショット集というものは意外と見つかりません。当たり前で、正規ルートではまず入れないし、改造コードで無理に飛ばしても、死に戻るたびに水中でも土管の外でもない中途半端な位置に吐き出されます。全てのスクリーンショットを撮るのは、思った以上に面倒くさいのです。
当時、9-1を見る方法はいくつかありました。最も有名なものは「テニス」を使う方法ですが、ファミリーベーシックでメモリにPOKEする手法もありました。「こちらにはもう nesemu と Claude がある」と気づいたのが、連載終わりの夜のことでした。
外から「ワールド番号」を書き換える
手順はシンプルです。タイトル画面を飛ばして、ゲーム開始直前に WorldNumber へ任意の値を書き込む。それだけ。ありがたいもので、この辺りは先人たちの知恵がWebにいくらでも転がっています。Claude も簡単のそれを見つけ出してくれました。
nesemu は JavaScript 製 NES エミュレータで、外部コントローラースクリプト (--controller オプション) を渡せば、フレームごとに CPU バスを読み書きできます。連載第 2 回で Claude 拡張を実装したときの道具がそのまま流用できました。
// extras/mario-256w/controller.js (抜粋)
const ADDR = {
OperMode: 0x0770, // 0=タイトル, 1=ゲーム中
WorldNumber: 0x075F,
LevelNumber: 0x075C,
NumberofLives: 0x075A,
}
case 'start':
bus.write8(ADDR.WorldNumber, targetWorld & 0xFF)
bus.write8(ADDR.LevelNumber, 0)
bus.write8(ADDR.NumberofLives, 9)
app.additionalPad |= PadValue.START // スタート押下
if (operMode === 1) { phase = 'release' }
break
OperMode の値を毎フレーム見張りながら、タイトル画面→ゲーム開始の境目で WorldNumber に 0〜255 のいずれかを書き込む。さらにゲーム中モードに入ってからも数フレームは上書きを続ける——ここが意外と要で、SMB は起動直後にもう一度 WorldNumber を読み直す処理があり、タイミングを 1 フレームずらすとあっさり 1-1 に戻されます。デバッガ付きで追ったというより、「なぜか 1-1 に戻る」を 10 回見て、とりあえず力技で実装しました。
自動巡回バッチ
1 面ぶんの起動コードができたら、あとはシェルで 256 回呼ぶだけです。
# extras/mario-256w/batch.sh (抜粋)
for (( w=WORLD_START; w<=WORLD_END; w++ )); do
WORLD=$(( w - 1 )) AUTO_EXIT=1 SCREENSHOT=1 \
timeout 30 npx ts-node nodejs/src/main.ts \
--controller "$CONTROLLER" --silent "$ROM" \
>> "$LOG_FILE" 2>&1
mv ss0.png "$OUTPUT_DIR/w$(printf '%03d' $w).png"
done
ヘッドレス実行、1 面あたり約 3 秒のファストフォワード、60 フレーム目でスクリーンショット、終わったら process.exit(0) で抜ける。256 面ぶんで実測 14 分 22 秒、失敗ゼロでした。再現性は嬉しい誤算で、うっかり「9-1 の途中でカートリッジを抜いたらフリーズするかも」のような昔の不安が、全部ソフトウェアの外に閉じ込められている感覚です。

画像だけでなく、1 面につき 3 秒の動画クリップも録画しました。controller.js の onRender コールバックで、PPU 出力の RGBA バッファを frames.raw に書き出しておき、あとで ffmpeg がまとめて mp4 にエンコードします。
ffmpeg -f rawvideo -pix_fmt rgba -s 256x240 -r 60 \
-i "$RAW_FILE" \
-c:v libx264 -pix_fmt yuv420p -crf 18 \
"$CLIP_FILE"
256 クリップを concat 結合すると、13 分弱の「256W 通し動画」になります。ファイルサイズは 6.1 MB。RAW RGBA の中間ファイルを面ごとに即削除する設計にしておかないと、HDD が 60 GB ほど刺さって死ぬので、そこだけは注意でした。
できたもの
出てきたのは、フォルダに整然と並んだ w001.png〜w256.png と、1 本の mp4。最初の 8 枚は見慣れた地上・地下・水中・城の風景で、9 枚目から先はもう別世界でした。崖から大気圏の外まで一気に持っていかれるような感触があります。
一面の雲が敷き詰められた面、土管が城壁を侵食する面、画面全体が真っ黒なのにBGM だけ鳴る面など、壊れているのに、ちゃんと美しい造形が出てくることがあって、ROM の廃墟を歩いているような気分になります。
技術そのものは第 1〜5 回の延長にすぎません。メモリに 1 バイト書く、ヘッドレスで起動する、スクリーンショットを撮る。個々の要素は地味です。ただ、AI と一緒に作った道具を、当時の想定外の領域に向けて使い倒すと、想定外の造形が大量に流れ出してくる。当時は攻略本の袋とじで見るしかなかった映像が、AIのおかげで簡単に得られるようになってしまいました。
☕ 余談 — 文化の保存
今回使っている ROM は自分で購入した中古カートリッジですが、ジャンクというわけでもなく 3-4 回に1回しか正常に起動しません。起動はしますがキャラクターに線が入ったり「バグった」状態になります。吸い出しでのエミュレーションなら、1回でも無事に動けばデータを吸い出して保存することができます。流石に 40 年も経つとハードウェアも劣化し、接点復活剤でも限界がありますね。
9-1 はもちろん、こうした「バグった」状態に良いしれぬロマンを感じたファンも多いのではないでしょうか。スーパーマリオ自体は繰り返し再販され、正常なゲームプレイはいまでも楽しめますが、いわゆる「裏技」を含めた思い出や文化は、いつまで生き延びることができるのでしょうか。

次回
仕組みの話はここまでで、次回は 256W のなかから面白かった面を紹介します。いわば番外編のカタログ。w024、w130、w175、w253——番号だけ書いても何も伝わらない世界がたくさんあるので、画面と一緒に、なぜそう見えるのかを memory map 側から少し手繰ってみます。
ROM の崖の向こう側に、見える絵が残っていました。




コメント