[PR] 本記事にはアフィリエイト広告(Amazonアソシエイト)を含みます。
難易度:中級 ★★☆(前提=BizHawk導入済み・Lua Consoleを起動できる状態/所要 約30分)
対象環境:Windows 10 / 11(64bit)、BizHawk 2.9系〜2.10系(EmuHawk)の Lua Console。生成AIは Claude を例に使いますが、指示の組み立て方は ChatGPT / Gemini でもそのまま通用します。BizHawkは更新が続いており、版によってLuaのバージョンやAPIの挙動が変わることがあります。本記事では「自分の版を先にAIへ伝える」ことを前提に手順を組んでいます。
【技術検証に関する法的注意事項・免責事項】
本記事に掲載されているメモリ解析、Luaスクリプト実行、およびAIコード生成に関する技術情報は、自らが所有するゲームソフトおよびオフライン環境における技術研究・教育目的の検証記録です。著作権法第47条の3に配慮し、複製ROMの配布、コピーガードの回避(DRM解除)、オンライン環境での不正操作(BOT化)等に関する情報は一切取り扱いません。掲載情報の実行によって生じた損害について、当ブログは一切の責任を負いかねます。
結論:AIに「一発で動くLua」を書かせる鍵は3点だけです
生成AIにBizHawk用のLuaを書かせて動かないとき、原因のほとんどは「あなたの環境をAIが知らない」ことです。指示に ①対象エミュレータと使ってよいAPI名の明示 ②入出力の型(アドレスのサイズ・エンディアン・戻り値) ③失敗したときの再指示の型、この3点を入れるだけで、初回から動く確率がはっきり変わります。逆に「BizHawkでHPを表示するLuaを書いて」とだけ頼むと、別のエミュレータのAPIが混ざったコードや、常駐ループの無い「一瞬で消えるコード」が返ってきます。
この記事では、その3点を埋め込んだコピペできるプロンプトのテンプレートを実物で3種類お渡しします。そのうえで、実際に「動かないコード」が返ってきてから直るまでの流れを、プロンプトと生成コードをそのまま並べて記録しました。
BizHawk本体の導入やLua Consoleの初期設定がまだの場合は、先にこちらを済ませてください。


1. なぜAIが書いたLuaは動かないのか(原因は5つに集約されます)
AIのコード生成が失敗する原因は、無限にあるように見えて、実際は次の5パターンにほぼ収まります。先にここを押さえておくと、返ってきたコードを読んだ瞬間に「これは動かないやつだ」と判断できるようになります。
原因1:他のエミュレータのAPIが混ざる
Luaでゲームを解析する文化はBizHawkだけのものではありません。FCEUX、Mesen、Snes9x-rr など、それぞれが独自のLua APIを持っています。学習データ上はそれらが混在しているため、AIはBizHawkには無い書き方を平気で出してきます。よくあるのが Mesen 系の emu.read(addr, emu.memType.cpu) です。BizHawkで実行すると、そんな関数は無いという趣旨のエラーで止まります。
紛らわしいのは memory.readbyte() です。これはBizHawkにも存在します。ただしBizHawkの memory.* は「直前に選択されているメモリドメイン」に対して読み書きする設計なので、ドメインを指定せずに呼ぶと、意図した領域を読めていないことがあります。読めているのに値が0のまま、という一番気づきにくい失敗はここから来ます。対策はシンプルで、メインメモリを読むなら mainmemory.read_u8() のようにドメインが確定した関数を使うようAIに指定することです。
原因2:常駐ループが無い
BizHawkのLuaスクリプトは、読み込んだ瞬間に上から実行され、最後まで到達すると終了します。gui.text() の描画はそのフレーム限りなので、ループが無いスクリプトは一瞬だけ文字が出て消えます。「実行しても何も表示されない」という相談の大半がこれです。
正解は while true do ... emu.frameadvance() end でフレームを進めながら描画し続けることです。emu.frameadvance() は「次のフレームまで待つ」役割を持っていて、これを入れずに while true を回すとエミュレータごと固まります。ループとフレーム進行はセットで指示する必要があります。
原因3:型とエンディアンの取り違え
HPが「255を超える」ゲームなら、その値は1バイト(0〜255)に収まりません。2バイトで読む必要があります。さらに、2バイト以上の値はバイトの並び順(エンディアン)が機種によって違います。
| 機種の例 | 並び順 | 使う関数 |
|---|---|---|
| NES / スーパーファミコン / ゲームボーイ系 | リトルエンディアン | mainmemory.read_u16_le() |
| メガドライブ / N64 | ビッグエンディアン | mainmemory.read_u16_be() |
ここを間違えると、値が「256倍ずれる」「HPを1減らしたのに数値が256飛ぶ」といった、いかにも不可解な挙動になります。バグに見えますが、単に読み方が違うだけです。AIは機種を教えられなければ推測で le を選ぶので、プロンプトに機種名とエンディアンを書いておくのが確実です。
原因4:AIが存在しないメモリアドレスを埋めてくる
これが一番やっかいです。「ドラクエ風RPGのHPアドレスを読むLuaを書いて」と頼むと、AIは 0x0075A0 のようなもっともらしい数値を勝手に埋めて返してきます。コードは動きます。しかし表示される数字はHPではありません。
メモリアドレスはソフトごと・リビジョンごとに違うので、AIが知っているはずがありません。プロンプト側で「アドレスは推測しないでプレースホルダを置いてください」と明示的に禁止する。これだけで、後から自分が特定した値を入れるだけの、正しい形のコードが返ってきます。
アドレスの特定そのものはAIの仕事ではなく、RAM Searchの仕事です。手順はこちらにまとめています。

原因5:座標系の食い違い
gui.text() はクライアント(ウィンドウ)座標に描画し、gui.pixelText() はエミュレートされた画面の解像度に対して描画します。表示倍率を2倍3倍と変えたときに文字だけ取り残されるように見えるのは、この違いによるものです。ゲーム画面の特定の位置に貼り付けたいなら gui.pixelText()、常に画面の隅に固定したいなら gui.text() を使う、と決めてAIに伝えます。
2. 指示テンプレ本体:そのままコピペして使えるプロンプト3種
ここが本記事の主役です。<山括弧>の部分だけ自分の環境に書き換えて、AIに丸ごと貼り付けてください。
テンプレA:新規にスクリプトを書かせる(ベーステンプレ)
最初の1回はこれを使います。長く見えますが、ここで環境を伝えておくと、以降の会話は短い指示で通るようになります。
# 役割
あなたは BizHawk (EmuHawk) の Lua スクリプトに詳しいエンジニアです。
# 実行環境
- エミュレータ: BizHawk <2.10>(EmuHawk / Windows 64bit)
- 実行方法: Tools → Lua Console からスクリプトを読み込む
- 対象コア/機種: <NES(リトルエンディアン)>
# 使ってよいAPI(これ以外は使わないでください)
- mainmemory.read_u8 / read_u16_le / read_u16_be / read_s16_le / read_u32_le
- gui.text(x, y, string) ※クライアント座標
- console.log(string)
- emu.frameadvance()
# 使ってはいけないこと
- FCEUX / Mesen / Snes9x など他エミュレータのAPI(emu.read など)を混ぜないでください
- メモリドメインを指定しない memory.readbyte() は使わないでください
- 私が指定していないメモリアドレスを推測で書かないでください
# やりたいこと
<指定したアドレスの値を、毎フレーム画面左上に表示し続ける>
# 入出力の型
- 入力: メモリアドレス(16進の整数)。スクリプト冒頭に定数として1箇所にまとめる
- 値のサイズ: <1バイト / 2バイト>(不明な場合は切り替えられるようにフラグを用意する)
- 出力: 画面への文字列表示のみ。ファイル書き出し・外部通信・メモリへの書き込みは行わない
# 制約
- アドレスは 0x000000 のプレースホルダにしてください(実値は私が入れます)
- while true do ... emu.frameadvance() end の常駐ループを必ず含めてください
- 1ファイルで完結させ、コメントは日本語で書いてください
- 動作確認すべき点をコード末尾にコメントで3つ挙げてください
# 出力形式
Lua のコードブロックのみ。前置きの説明は不要です。
効いているのは「役割」でも「丁寧な言い回し」でもありません。使ってよいAPIのホワイトリストと推測禁止の2ブロックです。この2つを外すと、原因1と原因4がそのまま返ってきます。
テンプレB:動かなかったときの再指示(デバッグテンプレ)
1回目で動かなくても、慌てて「動きません」とだけ送らないでください。それをやると、AIは原因を推測してAPI名ごと書き換え、前より遠ざかることが多いです。次の形で送ります。
# 状況
先ほどのスクリプトを BizHawk <2.10> の Lua Console で実行しました。
# 実際に起きたこと
<文字が一瞬だけ表示されて、すぐ消える>
# Lua Console の出力(原文のまま/改変していません)
<エラーが出ていればコンソールの表示を全文貼る。何も出ていなければ「出力なし」と書く>
# 私が確認済みのこと
- <console.log で値は取れている(0以外が出る)>
- <gui.text の座標を 10,10 に変えても変化しない>
# お願い
1. 原因の候補を、可能性の高い順に3つ挙げてください。
2. そのうち最も可能性が高いものだけを直した完全なコードを、差分ではなく全文で再掲してください。
3. API名を変更する場合は、それが BizHawk のどのバージョンで有効かを明記してください。
確証がなければ「確証なし」と書いてください。
ポイントは3つあります。エラー全文を原文のまま渡すこと(要約すると原因が消えます)、直す箇所を1つに絞らせること(同時に3箇所直されると、何が効いたのか分からなくなります)、そして「確証がなければそう書け」と逃げ道を用意することです。3つ目は、AIが自信満々に嘘のAPI名を出すのを抑える効果があります。
テンプレC:アドレス特定の「手順」を設計させる(答えを聞かない)
アドレスそのものをAIに聞くのは、原因4のとおり無意味です。代わりに絞り込みの手順表を作らせると、AIは非常に役に立ちます。
# 目的
<HP> のメモリアドレスを BizHawk の RAM Search で特定したい。
# 分かっていること
- 値の変化: <ダメージを受けると減る/回復すると増える/メニュー中は変化しない>
- 想定サイズ: <不明(1バイトか2バイト)>
- 画面上の数値表示: <ある/ない>
- 機種: <NES>
# お願い
アドレスを推測で答えないでください。
代わりに「ゲーム内でする操作 → RAM Search で使う比較条件」の手順表を、
上から順に実行できる形で作ってください。
各ステップに、その操作で候補が減る理由を1行だけ添えてください。
候補が減らなくなったときの分岐(サイズを変える/ドメインを変える)も最後に書いてください。
これは「AIに考えさせる範囲を、AIが本当に知っていることに限定する」という発想です。個別のアドレスは知らなくても、絞り込みの一般論は正確に知っている。この切り分けが、AI活用のコスパを大きく変えます。
3. 試行錯誤ログ:動かないコードが直るまで
以下は、同じ流れを手元で再現できるように整理した作業記録です。メモリアドレスは 0x00XXXX の伏せ字にしてあります(実値はソフトごとに違うため、あなたのRAM Searchの結果を入れてください)。実行中の画面キャプチャとGIFは、実測検証のうえ後日この記事に追記します。
ステップ1:雑に頼むと、こうなる
まず、あえて何も指定せずに頼んでみます。
BizHawkでHPを画面に表示するLuaスクリプトを書いて
返ってきたのが、次のようなコードです。
-- 1回目の生成コード(動きません)
local hp_address = 0x0075A0
local hp = memory.readbyte(hp_address)
gui.text(10, 10, "HP: " .. hp)
4行しかないのに、問題が4つ入っています。
- アドレス
0x0075A0は推測です(原因4)。何のソフトかも伝えていないのに具体的な値が入っている時点で、これは根拠がありません。 - 常駐ループが無いので、読み込んだ瞬間に1フレームだけ描画して終了します(原因2)。
memory.readbyte()をドメイン指定なしで呼んでいます(原因1)。狙ったメモリを読めている保証がありません。- 値の読み取りがループの外にあるので、仮にループを足しても数値が更新されません。これはループ追加時にやりがちな二次バグです。
ステップ2:テンプレAで頼み直す
同じ目的を、テンプレAの形で送ります。「対象コア/機種: NES(リトルエンディアン)」「値のサイズ: 不明(切り替えフラグを用意)」「アドレスは推測禁止」を埋めた状態です。返ってきたコードがこちら。
-- 2回目の生成コード(惜しい)
local ADDR = 0x000000 -- ★実アドレスに置き換える
local X, Y = 10, 10
while true do
local v = mainmemory.read_u8(ADDR)
gui.text(X, Y, "VALUE: " .. v)
emu.frameadvance()
end
推測アドレスは消え、常駐ループも入り、ドメインが確定した mainmemory.read_u8() になりました。指示を変えただけで、原因1・2・4が同時に消えています。ただし1つ抜けています。「値のサイズが不明なら切り替えフラグを用意する」がスルーされ、1バイト固定になっています。HPが255を超えるソフトでは、この時点で値が壊れます。
ステップ3:テンプレBで再指示する
ここでテンプレBを使います。実際に送った内容がこれです。
# 状況
先ほどのスクリプトを BizHawk 2.10 の Lua Console で実行しました。
# 実際に起きたこと
表示は出るが、HPが256以上になる場面で数値が0付近に戻ってしまう。
# Lua Console の出力(原文のまま)
出力なし(エラーは出ていません)
# 私が確認済みのこと
- 表示自体は毎フレーム更新されている
- 255までは画面の表示と一致している
# お願い
1. 原因の候補を、可能性の高い順に3つ挙げてください。
2. 最も可能性が高いものだけを直した完全なコードを、差分ではなく全文で再掲してください。
3. 1バイト読みと2バイト読みを、コード冒頭のフラグ1つで切り替えられるようにしてください。
「255までは合っている」という一文が効きます。これがあると、AIは読み取り幅の問題だと即座に特定できます。逆に「動きません」だけだと、座標やAPI名まで疑い始めて的が絞れません。どこまでは正しく動いているかを書くのが、再指示の一番のコツです。
ステップ4:最終コード(コピペ可)
再指示を経て確定したのが次のコードです。ADDR にRAM Searchで特定した値を入れ、2バイト値なら IS_16BIT を true にするだけで使えます。
-- watch_value.lua
-- BizHawk (EmuHawk) 2.9系〜2.10系 / Tools → Lua Console から読み込む
-- 目的: 指定アドレスの値を毎フレーム画面に表示する(読み取りのみ/書き込みは行わない)
-- 前提: 自分が所有するソフトを、オフライン環境で検証すること
local ADDR = 0x00XXXX -- ★RAM Searchで特定したアドレスに置き換える
local IS_16BIT = false -- 2バイト値なら true
local BIG_ENDIAN = false -- メガドライブ/N64など、ビッグエンディアンのコアなら true
local X, Y = 10, 10 -- 表示位置(クライアント座標)
local function readValue()
if not IS_16BIT then
return mainmemory.read_u8(ADDR)
elseif BIG_ENDIAN then
return mainmemory.read_u16_be(ADDR)
else
return mainmemory.read_u16_le(ADDR)
end
end
-- 起動時に、いま読んでいるメモリドメイン名をコンソールへ出す(読み違いの自己診断用)
console.log("watch_value.lua start / domain = " .. mainmemory.getname())
while true do
local v = readValue()
gui.text(X, Y, string.format("VALUE: %d (0x%X)", v, v))
emu.frameadvance()
end
-- 動作確認のポイント
-- 1) 値が常に0 → ADDR が違うか、メモリドメインが想定と違う(起動時ログを確認)
-- 2) 255で頭打ち/256以上で0に戻る → IS_16BIT を true にする
-- 3) 値が不自然に大きく飛ぶ → BIG_ENDIAN の設定を反転させて試す
末尾の「動作確認のポイント」は、テンプレAの「動作確認すべき点を3つ挙げてください」から出てきたものです。コード本体だけでなく、切り分け手順まで一緒に書かせておくと、次に詰まったときの往復が1回減ります。
このコードで値が見えるようになったら、次は画面上に常時表示するオーバーレイに発展させられます。

4. よくある失敗と、そのときの「再指示の一文」
症状から逆引きできる形にまとめました。右の一文をテンプレBの「お願い」欄に足すだけで、たいていは1往復で直ります。
| 症状 | 疑うべき原因 | 再指示に足す一文 |
|---|---|---|
| 文字が一瞬出て消える | 常駐ループが無い | 「while true do … emu.frameadvance() end の常駐ループで囲んだ全文を再掲してください」 |
| エミュレータが固まる | ループ内に emu.frameadvance() が無い | 「ループ内に必ず emu.frameadvance() を1回だけ入れてください」 |
| 値がずっと0のまま | アドレス違い/ドメイン違い | 「起動時に mainmemory.getname() の結果を console.log で出力する行を足してください」 |
| 値が更新されない | 読み取りがループの外にある | 「メモリ読み取りは必ずループの内側で毎フレーム行ってください」 |
| 256以上で値が壊れる | 1バイト読みのまま | 「1バイト読みと2バイト読みを冒頭のフラグで切り替えられるようにしてください」 |
| 値が不自然に飛ぶ | エンディアン違い | 「機種は〈機種名〉です。read_u16_le と read_u16_be のどちらが適切か理由付きで示してください」 |
| そんな関数は無いとエラー | 他エミュのAPIが混入 | 「BizHawkに存在しないAPIが混ざっています。mainmemory / gui / emu / console の4つのみで書き直してください」 |
| 倍率を変えると文字がずれる | 座標系の食い違い | 「ゲーム画面基準に固定したいので gui.pixelText を使ってください」 |
5. 応用:一度作ったテンプレを「環境カード」として使い回す
毎回テンプレAを全部書くのは面倒です。実務では、「実行環境」「使ってよいAPI」「使ってはいけないこと」の3ブロックだけを切り出してテキストファイルに保存しておき、新しい会話の1通目にそれを貼ってから本題を書く、という運用が楽です。この3ブロックを、ここでは環境カードと呼びます。
# 環境カード(会話の1通目に貼る)
- エミュレータ: BizHawk 2.10(EmuHawk / Windows 64bit)
- 実行: Tools → Lua Console
- 機種: NES(リトルエンディアン)
- 使ってよいAPI: mainmemory.* / gui.text / gui.pixelText / console.log / emu.frameadvance / event.onframeend
- 禁止: 他エミュレータのAPI混入、ドメイン未指定の memory.readbyte、アドレスの推測記入
- 出力: Luaコードブロックのみ、コメントは日本語、常駐ループ必須
以降の依頼はすべてこの前提で回答してください。
これを先頭に置いておけば、2通目以降は「所持金と座標を同時に表示するように拡張して」といった1行の依頼でも精度が落ちません。使うAIをClaudeからChatGPTに変えても、同じカードがそのまま機能します。効いているのはモデル固有の言い回しではなく、環境と制約を構造化して先に渡すという設計だからです。
なお、生成AIとエミュレータを同時に動かすと、ブラウザ側のメモリ消費が地味に効いてきます。試行錯誤の往復が増えるほど、作業環境の余裕が体感速度に直結します。
構成と価格帯の幅が広く、型番を1つに固定すると型落ち・在庫切れで記事がすぐ古くなる。ここは検索結果へ誘導し、選び方の基準は本文側で示す。
※価格・在庫・仕様は変動します。最新の情報はAmazonの商品ページでご確認ください。
6. よくある質問
Q. AIに聞けば、HPのメモリアドレスも教えてもらえますか?
いいえ、教えてもらえません。そして、聞くと「それらしい数値」が返ってくるので、かえって危険です。メモリアドレスはソフトごと・リビジョンごとに異なるため、AIが正解を持っているはずがありません。アドレスは自分でRAM Searchで特定し、AIには絞り込みの手順設計とコード生成を担当させる。この分担が、いまのところ一番失敗が少ないやり方です。
Q. これは「チート」ですか?
いいえ。本記事で扱っているのは、自分が所有するソフトをオフライン環境で動かし、メモリの値を読み取って観察する技術検証です。値の書き換えによる進行の有利化を目的とするものではなく、当ブログではROM・BIOS・改造ROM・セーブデータの配布、コピーガードの回避、オンライン環境での不正操作は一切取り扱いません。掲載しているコードも読み取り専用で、メモリへの書き込みは行っていません。
Q. Claude以外のAIでも同じテンプレは使えますか?
使えます。テンプレA〜Cで効いているのは、モデル固有の呪文ではなく「環境・API制限・型・禁止事項・出力形式」を構造化して渡すという形式です。ChatGPTでもGeminiでもそのまま貼り付けて機能します。ただしモデルによって、長いプロンプトの後半を軽く扱う傾向の差はあるので、一番守ってほしい制約(推測禁止・常駐ループ必須)は末尾ではなく中盤に置くと安定しやすいです。
Q. BizHawkのLuaのバージョンは、プロンプトに書くべきですか?
書いたほうが安全です。BizHawkは版によってLuaまわりの仕様が変わってきた経緯があるため、自分の環境のBizHawkのバージョンを明記するだけで、古い書き方が返ってくる確率が下がります。バージョンはEmuHawkのHelpメニューか、Lua Console起動時のログで確認できます。
Q. while ループではなく event.onframeend を使うべきですか?
どちらでも構いませんが、最初は while ループのほうが読みやすいのでおすすめです。event.onframeend() はコールバック登録型で、複数の処理を並行させたいときや、スクリプトを止めても登録が残る挙動を理解してから使うほうが安全です。AIに任せるときはどちらの方式で書くかを指定すると、生成が安定します。
7. まとめ
- AIが書いたBizHawk用Luaが動かない原因は、①他エミュAPIの混入 ②常駐ループ欠落 ③型・エンディアンの取り違え ④アドレスの推測記入 ⑤座標系の食い違いの5つにほぼ集約されます。
- プロンプトに「使ってよいAPIのホワイトリスト」と「推測禁止」の2ブロックを入れるだけで、①②④は初回から消えます。
- 動かなかったときは「動きません」ではなく、エラー全文+どこまでは正しいか+直すのは1箇所だけという形で再指示します。
- アドレスはAIに聞かず、RAM Searchで自分で特定する。AIには手順設計とコード生成をさせる。
- 「実行環境・使ってよいAPI・禁止事項」を環境カードとして保存し、会話の1通目に貼る運用にすると、以降は1行の依頼でも精度が保てます。
次回は、ここで作ったコードを土台に、AIエージェントにゲームを自動でプレイさせる実験に進む予定です。読み取りだけだった処理に判断と入力を足していくと何が起きるのか、同じように試行錯誤ログ付きで記録します。
次に読む
まだBizHawkを導入していない方は、まずここから。

このテンプレで生成したコードを、実際に動かす3記事です。



環境まわりの補足
Luaそのものを体系的に押さえておくと、AIが出したコードの良し悪しを自分で判断できるようになります。生成AIを使う場合ほど、読む力が効きます。
Lua作者陣による公式解説書。BizHawkのLua APIは断片的な情報が多く、言語そのものの仕様(テーブル・メタテーブル・コルーチン)を一度通しで押さえておくと、スクリプトが動かないときの原因切り分けが速くなる。
- ✓言語作者による公式解説
- ✓テーブルとメタテーブルの理解が進む
- ✓断片的なコピペから抜け出せる
※価格・在庫・仕様は変動します。最新の情報はAmazonの商品ページでご確認ください。
メモリ解析の考え方をもう一段深く知りたい場合は、リバースエンジニアリングの入門書が土台になります。
本ブログで扱うGhidraそのものを主題にした日本語書籍。逆アセンブル結果の読み方や関数の当たりの付け方が体系立っているので、記事の手順をなぞるだけの段階から一歩進める。
- ✓Ghidraを主題にした日本語書籍
- ✓逆アセンブル結果の読み方が体系的
- ✓記事の手順と地続きで学べる
※価格・在庫・仕様は変動します。最新の情報はAmazonの商品ページでご確認ください。
Amazonのアソシエイトとして、当ブログは適格販売により収入を得ています。
この記事の親ガイド

【予告】この5工程で使うスクリプトをまとめた「コピペで動くLua集+環境構築キット」を配布予定です。価格・配布日は未定で、決まり次第この記事に追記します。


コメント