こんにちは、パレイド技術部の夏目です。1984 年の Family BASIC に 2026 年のローカル LLM を接続し、AI に BASIC を書かせるベンチをつくる——という連載をリブートします。第 1 回の今回は、その手前にある地味な土台の話です。AI にコードを書かせる前に、まず「実機を自動で叩いて挙動を観察する道具」が必要で、それを Family BASIC V3 の ROM でも回るようにした、という達成報告になります。
ローカル LLM のベンチ数字は次回以降です。今回はその準備として踏んだ罠を、「構造設計」の視点で記録します。なお検証に使った ROM はいずれも実機のカートリッジを私的に購入し、自分で吸い出したもの(著作権法 30 条の私的複製)で、ROM ファイル自体の配布はしていません。
本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。
識別の罠と、その解消 ── ハッシュ一本化まで
手元には、自作の NES エミュレータ(nesemu)を改造した 2 つのハーネスがあります。1 つは Family BASIC を自動操作して命令辞典・リファレンスを作る「観察ハーネス」、もう 1 つはローカル LLM に書かせた BASIC をその場で走らせて採点する「ベンチハーネス」です。改造方針は、エミュレーションコアには手を入れず、識別と操作の層だけで完結させること。CPU・PPU・APU といった中身はそのままに、周辺機器を有効化する識別の判断とハーネス側の制御だけを足しています。
従来このハーネスは Family BASIC v2.0a / v2.1a を基準に組んでありました。今回はこれを V3(v3.0 ROM)でも回るようにします。ところが最初の一歩で止まりました。このエミュレータは Family BASIC を「ROM ファイル全体の MD5」で識別し、ハッシュが登録済みのものと一致したときだけ、キーボードなどの周辺機器を有効化します。MD5 はファイルの中身から一意に決まる短い指紋(ハッシュ値)で、1 バイトでも違えば別物になります。
問題は、ハッシュの対象が「カートリッジ本体」ではなく「ファイル全体」だったことです。NES の ROM ファイルは先頭に iNES ヘッダ(16 バイトのメタデータ)を持ちます。これはカートリッジの中身ではなく、吸い出しツール(ダンパ)が付与する付帯情報です。つまり同じゲーム本体でも、吸い出したツールが違えばヘッダが変わり、ファイル全体の MD5 も変わる。実際、手元では次のようにずれていました。
| 版 | ダンプ元 | ファイル全体の MD5 | 登録状態 |
|---|---|---|---|
| v3.0 | 公式 dump | 2ba1dbbb774118eb903465f8e66f92a2 |
登録済 |
| v3.0 | RetroFreak 吸い出し | c4e1a45d2adb9a9fee63a795c1bd4c08 |
未登録(別物扱い) |
| v2.0a | 公式 dump | fc1668b… |
登録済 |
| v2.0a | RetroFreak 吸い出し | ef3b2aeb… |
未登録(別物扱い) |
「同じ v3.0 なのにハッシュが違うから周辺機器が有効にならない」というのが、最初の足止めでした。原因が分かれば対処は簡単で、手元の dump のハッシュを識別表に登録するだけです。問題は、その登録先が壊れた設計になっていたことでした。
ハッシュを判定する分岐は、ブラウザ版エントリと CLI 版エントリの 2 か所に重複していました。新しい dump を 1 つ足すたびに、2 か所のコードを同じ内容で直さなければならない。これは新版対応のたびに片方を直し忘れる事故が約束された設計です。実際、観察ハーネス(CLI 側)だけ V3 を通したらブラウザ側で認識されない、という食い違いが起きていました。
そこで、ハッシュ → 版ラベルの対応表を束ねる共通モジュールを新設し、単一ソースにまとめました。版ラベルを返す関数と、Family BASIC かどうかを判定する関数の 2 つを公開し、ブラウザ版・CLI 版のエントリはこの関数を呼ぶだけにします。これでハッシュの真実は 1 か所に集約され、新しい dump への対応は対応表に 1 行足すだけで両ハーネスに反映されます。
この共通モジュールはコアの挙動を変えるものではありません。エミュレーションの中身はそのままで、「どの ROM を Family BASIC と見なし周辺機器を有効化するか」という識別の責務を、重複していた 2 か所から 1 つに引き上げただけです。ブラウザ版と CLI 版の双方から参照できる共通の場所に置いたので、コアの判定ロジックに割り込んだわけではありません。
| 役割 | 変更内容 |
|---|---|
| 識別モジュール(新規) | ハッシュ→版の単一ソース。版ラベルを返す関数と判定関数を公開 |
| ブラウザ版エントリ | 分岐を判定関数の呼び出しに置換 |
| CLI 版エントリ | 版ラベルを返す関数を呼び、版ラベルを保持 |
| 観察データ型 | 観察データの型に版フィールドを追加 |
| 観察ハーネス | 解決した版ラベルを観察 JSON に stamp |
| ベンチハーネス | V3 起動フォールバックを移植 |
| 一括実行スクリプト | 環境変数で ROM パスを差し替え可能に |
起動の版差と観察データの版管理 ── ここで一度はまる
識別を通しても、ベンチハーネスはまだ V3 で動きませんでした。原因は起動手順(画面が切り替わる順番)が版で違うことです。v2 系は起動時に「1–BASIC / 1,2,3」のメニューが出て、1 を押して BASIC に入る必要があります。ところが V3 はこのメニューが無く、いきなり BASIC の OK プロンプトに入る。ベンチ側は v2 を前提にメニュー検出へ依存していたため、V3 では出てこないメニューを永遠に待ち続け、タイムアウトで FAIL していました。
対処は、版に依存しないフォールバックの移植です。「メニューが出なければ OK プロンプトを検出して直行する」という分岐を入れました。観察側はこの分岐を既に持っていたので、同じ考え方をベンチ側にも揃えた形です。ログには「メニューなし(V3)、OK プロンプト直行」の旨が記録され、メニュー無しの直行が確認できます。
| 版 | 起動直後 | 旧ベンチの挙動 | 対処後 |
|---|---|---|---|
| v2.0a / v2.1a | 1–BASIC メニュー | 1 を押して BASIC へ ○ |
変わらず ○ |
| v3.0 | いきなり OK プロンプト | メニューを待ち続けタイムアウト × | OK プロンプト検出で直行 ○ |
もう 1 つ、観察データの版管理を直しました。ハーネスが書き出す観察 JSON には「どの版で観察したか」のフィールドが無く、v2.1 の観察物と v3 の観察物が区別できない状態でした。これは命令辞典を版ごとに編み直すうえで致命的です。そこで観察データの型に版フィールドを追加し、ROM ハッシュから解決した版ラベルをハーネスが stamp するようにしました。あわせて一括実行スクリプトの ROM パスを環境変数で差し替え可能にし、v2.1 と v3 を並行して観察できるようにしています。
検証は文字列表示の probe(print-string)を両版で走らせて確認しました。実際の画面出力はこうなります。
OK
10 PRINT "HELLO"
RUN
HELLO
OK
V3 ROM で走らせた観察 JSON には版ラベル v3.0 と正常終了が入り、画面に HELLO が出ました(ログには先述のメニュー無し直行も残ります)。続けて v2.0a ROM で同じ probe を走らせると版ラベル v2.0a で正常終了し、回帰(従来動作が壊れていないこと)も確認できました。両版が別ラベルで記録され、片方を直してももう片方が壊れないことまで見て、今回の対応を一区切りとします。
まとめと、次回に続く
「V3 ROM でハーネスが回るようになった」までが今回の到達点です。コアエミュには触らず、識別層とハーネス側の対応だけで V3 の入り口に立てました。
次回以降に積み残したのは中身の更新です。1 つは命令辞典・リファレンスのコーパスを V3 で観察し直して反映すること。V3 は RAM が 4KB に増え、BGGET / BGPUT / ON ERROR / INSTR といった V3 固有命令も増えています。もう 1 つは、その V3 コーパスを使った LoRA 対応です。実機観察のデータをモデルの重みに焼く方向を、版を揃えたうえで試します。次回は、V3 固有命令の観察から再開します。