こんにちは、パレイド技術部の夏目です。動画「ファミリーベーシックでマシン語に挑戦【前編】」の受け記事です。ファミリーベーシック V3 の BASIC で AI に作らせたテトリス風の落ち物パズルは、動きはしたものの、エミュレータで測ると1行落ちるのに約16秒かかりました。マシン語(BASIC から CALL 命令で呼び出す 6502 の機械語)で書き直すと、入力を1フレームごとに読めるようになりました。
この記事には、動画で語り切れなかった実測の表、踏んだ罠、公開したコードの打ち込み方をまとめます。マシン語の命令を教える記事ではなく、画面と音を直接動かす手段としてマシン語を使った記録です。数字はすべてエミュレータ上の実測で、実機・実テープでは確認していません。
https://youtu.be/USsr0Ff_6Tc
本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。
BASIC 版は1行落ちるのに約16秒
測定の条件は次のとおりです。フレームは画面1枚ぶんの時間の単位で、エミュレータが垂直ブランク(後述)を迎えた回数で数えました。
| 項目 | 内容 |
|---|---|
| 対象 | ファミリーベーシック V3(エミュレータ上) |
| 時間の単位 | フレーム(1秒 = 約60.1フレーム) |
| 条件 | 無操作、最初の約3,600フレーム(約60秒)、6周ごとに1行落ちる設定 |
| BASIC 版 | AI に作らせた LOCATE/PRINT 版と、画面への書き方を PPU 直接書き込みに変えた版 |
| マシン語版 | 盤面・操作・得点を BASIC 版と揃えた 2,029 バイト |
| 項目 | LOCATE/PRINT 版 | PPU 直接書き込み版 | マシン語版 |
|---|---|---|---|
| 入力を読む間隔 | 71.5フレーム(1.19秒) | 101.0フレーム(1.68秒) | 1フレーム |
| 盤面の全面再描画(180マス) | 559フレーム(9.3秒) | 341フレーム(5.7秒) | 5フレーム |
| 1行落ちるまで | 988フレーム(16.4秒) | 947フレーム(15.8秒) | 30フレーム(設定値) |
| 測定時間のうち全面再描画の割合 | 53% | 33% | – |
| 判定 | × 遊べない | × 遊べない | ○ |
PPU(画面を描く専用チップ)に直接書くと、全面再描画は 559 から 341 フレームに縮みました。ところが1周は 71.5 から 101.0 フレームに延び、1行落ちる時間は 16.4 秒と 15.8 秒でほぼ同じです。スコア表示を1文字ずつ POKE で書く処理などが重く、BASIC の中で書き方を変えても桁は変わりませんでした。
遅さの元は、ミノが1段落ちるたびに盤面の180マスを1マスずつ書き直していることです。LOCATE/PRINT 版では測定時間の53%がこの全面再描画に消えていました。ミノの移動に要るのは、消す4マスと描く4マスだけです。
- 物差し: 製品版 NES の落ち物パズル(テトリス)は、最も遅いレベル0で48フレーム/行です(Tetris Wiki)。BASIC 版の 988 フレームはその約20倍(988÷48≒20.6、計算値)
- マシン語版との倍率は、入力が約72倍、全面再描画が約112倍、1行落ちるまでが約33倍。33倍はマシン語側の落下設定(30フレーム/行)で決まる値で、速さの上限ではない
- 測定の限界: 周回を数える印の
POKE/PEEKを足しているため、原版よりわずかに遅い(影響は未測定)。キー入力の取りこぼし率も未測定
マシン語版で変えたのは「いつ、どこに書くか」
ファミコンの画面と音は、それぞれ専用の回路が受け持っています。登場する用語を先に整理します。
| 用語 | 意味 |
|---|---|
| VRAM | PPU が読む画面用のメモリ。どこにどの絵柄を置くかが入る |
| 垂直ブランク | 画面を上から下まで描き終えて、上へ戻るまでの短い時間 |
| NMI | 垂直ブランクの始まりに、PPU が CPU を呼び出す割り込み(Non-Maskable Interrupt) |
| APU | 音源(矩形波2つ・三角波・ノイズ・DMC)を受け持つ回路(Audio Processing Unit) |
VRAM に書けるのは、画面を消している間か垂直ブランクの間だけです(NESdev Wiki)。垂直ブランクは約2,273 CPU サイクルで、CPU の 1.79MHz で割ると約1.3ミリ秒しかありません(どちらも資料の値からの計算値)。描いている最中に書き換えると、1枚の中で新旧が混ざって画面がちぎれます。
そこで処理を2つに分けました。メインループでゲームの計算をして、書きたい場所と値を RAM のキュー(順番待ちの列)に貯め、NMI の中ではキューの中身を PPU のレジスタ $2006(アドレス)と $2007(データ)へ流すだけにします。ミノが動くときに書くのは8マスで、ライン消しのあとの全面再描画も NMI が1フレームに5行ずつ書いて5フレームで終わります。600フレーム測ると、垂直ブランクの時点でメインループが待っていた割合は100%で、計算は毎フレーム間に合っていました。
ファミリーベーシック固有の注意もあります。NMI の飛び先は ROM ではなく RAM の $00ED にあり、書き換えると BASIC の画面更新が止まります。BASIC が $32・$33 に持つ画面設定を自前の NMI で書き戻さないと、画面は真っ黒になります。この前提とソースの断片は、機械語対応(2)のリファレンスにまとめてあります。
音も、APU に割り当てられたアドレスへ値を書くだけです。BASIC の PLAY は演奏が終わるまで次の行に進みません。PLAY "CDE" の直後の POKE が実行されたのは約118フレーム目で、最後の音源への書き込み(約85フレーム目)より後でした。ゲームを止めずに鳴らすには、NMI で毎フレーム少しずつ演奏を進める必要があります。マシン語版の割り当ては次のとおりです。
| 音源 | 役割 |
|---|---|
| 矩形波1 | 効果音(回転・固定・ライン消し・ゲームオーバー)と、和音のアルペジオ(根音→3度→5度を4フレームごと) |
| 矩形波2 | 旋律 |
| 三角波 | ベース(根音と5度を8分音符で交互に) |
| ノイズ | 裏拍のハイハット |
| DMC | 使わない |
回転の効果音は $8A,$08,$E0,$08 の4バイトを書くだけです。矩形波1を効果音と和音で共有しているため、効果音が鳴っている間は和音を休ませます。BGM はロシア民謡「コロベイニキ」の最初の8小節を繰り返すもので、旋律は記憶から起こしました。音の高さと長さが設計どおりかは APU への書き込みを記録して確かめ、動画では伴奏つきで鳴らしています。
実装中に踏んだ罠は次の3つです。
BPL(直前の結果が負でなければ分岐する命令)でループを組むと、数える値が128以上のとき最初の1回で抜ける。行ずらしが動かず、ライン消しが無限ループになった。0まで数える形に直した- 枠のタイルを書く前に、A レジスタ(CPU が計算に使う置き場)を別の値で上書きしていた。右の枠が「,」の字になった
- 分岐命令が届くのは ±127 バイトまで。超えたら
JMPで中継する
公開コードを打ち込んで動かす
ファミリーベーシック V3 に打ち込める形で、BASIC のコードを pareido.jp に置きました。マシン語は DATA 文に16進で入っていて、POKE でメモリに置き、最後に CALL で呼びます。説明は README にあります。
| ファイル | 中身 | 先頭の行 |
|---|---|---|
| tetris_01.bas | マシン語の1パート目 | CLEAR &H6B50 |
| tetris_02.bas | 2パート目 | CLEAR &H6826 |
| tetris_03.bas | 3パート目。最後の CALL &H6808 でゲームが始まる |
CLEAR &H6808 |
| tetris_basic_slow.bas | AI に頼んだ BASIC だけの版(比較用の遅い方) | – |
# 1パート目を打ち込んで実行。DATA をメモリに置き終わるまで待つ
tetris_01.bas を入力 → RUN
# BASIC のプログラムを消して、次のパートへ
NEW
tetris_02.bas を入力 → RUN
NEW
# 最後のパートの CALL でゲームが始まる
tetris_03.bas を入力 → RUN
先頭の CLEAR は、BASIC が使うメモリの上限を決める命令です。使える RAM は $6000〜$6FFF の4KBだけで BASIC の本文と共有するため、後のパートほど置き先を下げ、次のパートの BASIC 本文が置いたマシン語を上書きしないようにしています。値は変えないでください。
操作は ←→ で移動、↓ でソフトドロップ(1段 +1点)、A で回転、START でやり直しです。BASIC 版との違いもあります。マシン語版は画面の上にはみ出して固定されるとゲームオーバーになり(BASIC 版は積み上がっても終わらない)、音があり、落下は30フレーム/行です。エミュレータへ自動でタイプ入力した場合、打ち込みから起動まで469秒でした。実機での打ち込みと、テープへの SAVE/LOAD は試していません。
まとめ
BASIC で AI に作らせたテトリス風は、1行落ちるのに約16秒かかりました。マシン語版は入力を1フレームごとに読み、全面再描画は5フレームで終わります。差を生んだのは命令の速さだけでなく、垂直ブランクに書く量を8マスまで絞り、音を NMI で進めるという、ファミコンの仕組みに沿った書き方です。
未確認のまま残っていることは次のとおりです。
- 実機・実テープでの動作と速度
- 測定用の印による遅れの大きさと、入力の取りこぼし率
同じ方法でスーパーマリオ風の 1-1 を作った後編は、別の記事にしました。
パレイドファミリーベーシックでマシン語に挑戦【後編】マリオ風 1-1 を 4KB に入れるこんにちは、パレイド技術部の夏目です。ファミリーベーシック V3(任天堂、1985年)の上で、AI にマシン語(CPU 6502 が直接実行する命令列)を書か…連載の出発点「なぜ機械語か」はこちらです。
動画の音声: VOICEVOX:冥鳴ひまり