Appearance
システム構成・エンジン設計
実装の正典は src/engine.s(ゲームロジック)+ src/sound.s(BGM ドライバ)。本書はその構造の解説です(main 2026-09-02 時点、build/engine.nes = 131,088 バイト)。
ROM / ハードウェア構成
| 項目 | 内容 |
|---|---|
| マッパー | mapper 2(UNROM)。バンク 0〜6 = データ、固定バンク($C000-$FFFF)= コード・UI フォント・サウンドドライバ・DPCM |
| PRG-ROM | 128KB(8 × 16KB、全バンク出力・$FF パディング) |
| CHR | CHR-ROM なし。CHR-RAM 8KB にリセット時転送(起動エントリで内容を差し替え) |
| ミラーリング | 垂直(固定 1 画面のため実質未使用) |
| 画面 | 固定 1 画面(スクロールなし) |
| リンカ設定 | cfg/nes.cfg(NROM テスト ROM は cfg/nrom.cfg) |
| iNES ヘッダ | 4E 45 53 1A 08 00 21 00 |
PRG バンク割り当て
| バンク | 内容 |
|---|---|
| BANK0 | 全 5 曲の BGM データ(bgm_data.inc)+ 譜面・拍ターゲット表・ポインタ表(charts_data.inc)+ タイトル画面一式(#86: シーンコード update_title 系・メニュー行・一枚絵 CHR/NAM/ATR/PAL・呼吸パレット表 = title_picture.inc、約 5.5KB)。タイトル BGM(曲 0)のバンクと同居させることで、BGM 再生中も NMI の snd_bank 復帰でこのバンクが mapped のまま保たれる(.assert BGM_SONG0_BANK = TITLE_CODE_BANK) |
| BANK1 | 観客タイル列テーブル(17 状態 × 4 フレーム × 168B = 11,424B) |
| BANK2 | 講師 CHR 4,096B + コーチストリーム雛形 + AI CHR 3,328B + ノーツ/マーカー/紙吹雪 CHR + AI ゲームオーバー CHR |
| BANK3 | production は空。DEBUG_MENU ビルドではデバッグ状態機械 |
| BANK4 | CLEAR 結果描画・ランク計算・進行更新・紙吹雪・S ランク演出 + シーンコード(PROLOGUE / GAME OVER メニュー / CLEAR の START 処理。PROLOGUE_CODE_BANK、DEBUG ビルドでは BANK3) |
| BANK5 | プロローグフォント CHR |
| BANK6 | エンディングフォント CHR |
| PRGFIX | エンジン本体(CODE 約 6.7KB)+ RODATA + DPCM サンプル(0x940)+ ベクタ。空きは約 630B(DEBUG 約 570B、#86 でタイトルを BANK0 へ移設して確保) |
UNROM バス競合
バンク切替は ROM 上に 0〜7 が並ぶ snd_banktable への同値書き込み(lda table,x / sta table,x)で行う。固定バンクのコードが読むデータを切替バンクに置かないこと(song_bit_masks を BANK4 に置いて曲選択が無限ループした実績あり)。
エンジン全体構造
リセット
├─ RAM クリア($0C-$11 の進行レコードは保護)→ 進行レコード検証(magic $C3/$3C + 範囲チェック)
├─ progress_entry_request に応じて CHR-RAM 転送
│ (UI フォント / ステージ BG / 会話かなフォント or プロローグ・エンディングフォント / スプライト。
│ TITLE・PROLOGUE は $1000 に一枚絵、スプライトシートは載せない)
├─ パレット設定(BG 4本 + スプライト 4本)
├─ sound_init → エントリ別の初期 BGM(タイトル=曲0 / COUNTIN=本番開始 / ENDING=曲4)
└─ メインループ(フロー状態機械)
├─ nmi_flag 待ち(1フレーム同期)・パッド読み取り(pad_cur / pad_prev / pad_new)
├─ START+A+B → タイトルへソフトリセット(タイトル以外)
├─ flow_state ごとの更新(下表)
├─ ノーツレーン描画・キャラアニメ・コーチ CHR ストリーミングのパッチ
├─ 4 拍ごとの集計 → 観客増減 → CLEAR / ゲームオーバー判定
└─ 描画要求を ppu_buf に積む(高負荷処理は *_pending で次フレームへ分離)
NMI(vblank)
├─ OAM DMA
├─ ppu_buf フラッシュ
├─ コーチ CHR 半フレーム転送(RAM 上の展開コードを実行、64B/vblank)
├─ sound_tick(ポーズ中は停止)
└─ frame_count++ / nmi_flag セット / バンク復帰ゲームフロー状態機械(flow_state)
| 値 | 状態 | 画面 | 進行 | 遷移先 |
|---|---|---|---|---|
| 9 | FLOW_TITLE | タイトル一枚絵(256×160、タイル行 0〜19、#86)+ 行 20 黒 + メニュー 4 行(行 22 START / 24 CONTINUE / 26 PROLOGUE / 28 難易度行、col 0 の - がカーソル)。全曲クリア済み(mask=%11111)なら CONTINUE 行は STAGE SELECT 表記(#87)。ロゴとポータルのパレットが 12f × 8 ステップで「呼吸」(下記)。アイ・コーチ・観客は出ない | 上下・A、←→/SELECT で NORMAL⇄EASY | START→8 / CONTINUE→6 / STAGE SELECT→ENTRY_SONG_SELECT で reset → 11 / 10 秒無入力→10(アトラクト) |
| 10 | FLOW_PROLOGUE | プロローグ: 全幅の絵(256×160、タイル行 0〜19)+ 3 行テキスト(行 22/24/26)× 4 ページ | 240f で自動送り、A で次ページ、START でタイトル | 各ページは progress_prologue_page を永続化して jmp reset で切替。4 ページ後にタイトルへ |
| 8 | FLOW_ENTRANCE | コーチが左端から歩いて入場 | 自動 | x=88 到達で 0 |
| 0 / 1 | FLOW_DIALOG_1/2 | 会話ページ 0 / 1(24 タイル × 4 行、8f/行) | A | 1 → 4 |
| 4 | FLOW_CLEAR_TO_TUTORIAL | 会話行を HUD へ戻す(2 行/frame) | 自動 | 2 |
| 2 | FLOW_TUTORIAL | 6 ノーツ練習(BGM 停止・メトロノーム) | 方向/A/B、SELECT でスキップ | 譜面 $FF or SELECT → 3 |
| 3 | FLOW_DIALOG_3 | 会話ページ 2 | A | 5 |
| 5 | FLOW_CLEAR_TO_PERFORMANCE | 同上 | 自動 | start_performance → 6 |
| 6 | FLOW_PERFORMANCE | 本番(LETS DANCE バナー 180f → カウントイン 96f → BGM 開始) | 方向/A/B、START=ポーズ、SELECT=曲送り | 譜面 $FF → 7 / 観客 0 → ゲームオーバー(下記) |
| 7 | FLOW_CLEAR | RANK: X + プロンプト行 | START | ソフトリセット経由で 6(次曲 or リトライ)/ 初回 ALL CLEAR は 12 / 選曲プレイのクリア(RESULT_SELECT)は 11 |
| 11 | FLOW_SONG_SELECT | 曲選択(全曲クリア後のタイトル STAGE SELECT、または選曲プレイのクリア後に ENTRY_SONG_SELECT で直接) | 上下(プレビュー再生)・A、B でタイトルへ(#93) | 6 / B→9(reset 経由) |
| 12 | FLOW_ENDING | 曲 4 + エピローグ 12 ページ × 3 行 | A / 300f 自動 | 最終ページで A or START → 9 |
起動エントリ progress_entry_request: 0=TITLE / 1=OPENING(入場) / 2=COUNTIN(本番直行) / 3=PROLOGUE_MANUAL / 4=PROLOGUE_AUTO / 5=ENDING / 6=SONG_SELECT(#87)。進行レコードの検証は ENTRY_REQUEST_COUNT(7) 未満を正とする。画面をまたぐ遷移は jmp reset で行い、reset がこの値を消費して CHR とサウンドを初期化する。
ゲームオーバー(game_state=1、#87): flow_state は 6 のまま。発火フレームは文字を積まず、空席再描画(6 フレーム、1 行/フレーム)が終わってから切替バンクの update_gameover_menu が row 7 GAME OVER / row 9 CONTINUE / row 11 TITLE を 1 フレーム 1 行で描く(24 タイル全幅行で JUDGE ラベル等を上書き、col 0 の - がカーソル)。上下で選択、A または START(いずれも pad_new)で決定: CONTINUE=ENTRY_COUNTIN(同じ progress_current_song の本番)/ TITLE=ENTRY_TITLE(進行レコードは保持されるのでタイトルの CONTINUE も同じ曲)。main loop は BGM 停止中の snd_bank にシーンコードバンクを入れて呼び出す(PROLOGUE と同方式)。カーソルと行ステージングはタイトル用の title_selected / scene_draw_row を流用(ZP 追加なし)。
進行システム(曲送り)
- 進行レコード(ZP
$0C-$11): magic 2B /progress_current_song/progress_cleared_mask/progress_entry_request/progress_easy_mode。reset の RAM クリアから除外され、ソフトリセットをまたいで保持(電源断で消える) - CLEAR 時 ランク A 以上で現在曲のビットを立て、次の未クリア曲へ前進。A 未満は同じ曲でリトライ
- 初期 mask は
%10000(譜面なしの曲 4 をクリア済み扱い)。%11111で ALL CLEAR → エンディング - エンディングは初回だけ(#87):
update_clear_progressは OR する前の mask が既に%11111ならRESULT_SELECTを返し、CLEAR 画面(STAGE SELECT - START)の START はENTRY_SONG_SELECTで選曲画面へ戻る。CLEAR の START 分岐は切替バンクのclear_result_entries(RETRY/NEXT→COUNTIN、ALL_CLEAR→ENDING、SELECT→SONG_SELECT)の表引き - EASY モード: 偶数 index の拍(各小節 1・3 拍目)を強制 REST 化(
$FF終端は対象外、チュートリアルには効かない)
隠しコマンド
| 場所 | 入力 | 効果 |
|---|---|---|
| タイトル | ← → ← → ↑ ↓ B B | 全曲クリア扱い(CONTINUE 行が STAGE SELECT に変わり曲選択に入れる)。白フラッシュ + 歓声(update_cheer / update_audience_flash はタイトル経路から明示的に呼ぶ #86) |
| 本番ポーズ中 | A+B+SELECT 同時押し | GOOD/MISS を 0 にして即 CLEAR = RANK S |
| タイトル以外どこでも | START+A+B 同時押し | タイトルへソフトリセット(直後の 1 フレームはパッドを捨てて押しっぱなし誤検出を防止 #69) |
PPU アクセス規約(重要)
初期化後、メインスレッドは $2007 に直接触らない。 すべての PPU 書き込みは ppu_buf($0300、96 バイト、[len, addr_hi, addr_lo, data...]* 0)に積み、NMI がフラッシュする。
- 最悪ケース試算は
engine.s冒頭コメントに記載(ゲームプレイ 69B / ポーズ解除 82B / CLEAR 67B / ゲームオーバー 32B / タイトル 51B = 行 28 + 呼吸パレット 18 + 隠しコマンドの backdrop 4 + 終端 1)。ppu_buf を増やす変更は試算コメントも更新する - 1 フレームに収まらない処理は
clear_pending/restart_pending/audience_queue_pendingで次フレームへ分離(#68 の崩壊対策)。queue_audience自身にも空き容量ガードあり - テキスト行は 1 フレーム 1〜2 行に分割して積む
コアループ: ノーツレーン
お手本ガール(コーチ)とプレイヤーが同じ目標拍で一緒に踊る。
| 定数 | 値 | 意味 |
|---|---|---|
NOTE_APPROACH_FRAMES | 96 | ノーツは目標拍の 96f 前に出現 = カウントイン長。song_frame が 96 に達した瞬間に BGM 開始 |
NOTE_SPEED | 2 px/f | 右から左へ(96f × 2px = レーン幅 192px) |
MAX_LANE_NOTES | 6 | レーン上の未判定ノーツ最大数(+ 判定マーカー 1 枚 = 同一 Y に 7 枚、8 枚/スキャンライン制限内) |
FRAMES_PER_BEAT | 32 | チュートリアルとアイドルアニメ位相にのみ使用。本番は曲別テーブル |
ANNOUNCE_FRAMES | 180 | LETS DANCE バナー |
- 譜面は 1 拍 1 バイト(0=REST / 1=L / 2=R / 3=U / 4=D / 5=A / 6=B)、
$FF終端。拍数はlen(order)*16固定で、未作成部分は休符パディング - 拍ターゲット
target[N] = 96 + ceil(N × 4 × speed8.8 / 256)をtools/export_charts_inc.pyがサウンドドライバと同じ丸めで事前計算(16bit)。行カウンタは共有しないが結果として拍とノーツが一致する - 曲頭・曲末の休符パディングは
scan_silentを立てて集計・描画なしで一気にスキャン(これがないと ppu_buf が溢れる #35/#68) - OAM: アイ
$0200-$021F/ コーチ$0220-$023F/ レーン$0240-$025B(CLEAR 時は紙吹雪 8 枚が同じ枠を使用)
楽曲一覧
| # | 曲 | BPM | f/拍 | 拍数 | ノーツ | 長さ | 役割 |
|---|---|---|---|---|---|---|---|
| 0 | サニーステップ | 120 | 30.00 | 64 | 43 | 32.0s | 新規スタート曲 / タイトル BGM |
| 1 | ネオン・ミッドナイト | 128 | 28.12 | 144 | 102 | 67.5s | |
| 2 | グルーヴ・サーキット | 116 | 31.05 | 128 | 91 | 66.2s | |
| 3 | クリムゾン・オーバードライブ | 152 | 23.69 | 176 | 128 | 69.5s | |
| 4 | トワイライト・スターズ | 96 | 37.52 | 112 | 0 | 70.0s | エンディング専用(譜面なし。曲選択・本番曲送りではスキップ) |
CHARTED_SONGS_MASK = %00001111。譜面は assets/charts/charts.json(譜面メーカー #45 で編集)→ charts_data.inc。
判定システム
チューニングは engine.s 冒頭の定数で完結する。
| 定数 | 値 | 内容 |
|---|---|---|
JUDGE_PERFECT | 4 | 目標拍 ±4f → PERFECT +100 |
JUDGE_GOOD | 8 | 目標拍 ±8f → GOOD +50 |
AUD_INIT / AUD_MAX | 8 / 16 | 観客の初期値・上限(0 でゲームオーバー) |
AUD_LV4_MIN | 13 | Lv4 ハイプ(A/B の PERFECT で歓声 12f + 白フラッシュ 2f) |
- 押した振りは判定に関係なく即座に再生(タイミング外・誤ボタンでも踊る)
- 誤ボタンは MISS でノーツ消化、目標 +9f 経過で無入力 MISS
- 窓外の空振り・譜面終了後の入力は MISS 表示のみでノーツを消化せず、集計・ランクにも入らない(#62)
- フレーズ集計は判定カーソルが 4 進むごと(休符も数える): 全 PERFECT → 観客 +2 / MISS ≤1 → +1 / MISS ≥2 → −2。チュートリアル中は増減・スコア加算なし
- 観客表示は 17 状態 × 4 アニメフレーム・3 段 84 席。テンション Lv1=1-4 / Lv2=5-8 / Lv3=9-12 / Lv4=13-16。1 フレーム 1 行(28 タイル)ずつ描画
ランク(実判定数比)
| ランク | 条件 |
|---|---|
| S | MISS 0 かつ GOOD 0(全 PERFECT) |
| A | MISS 0(GOOD あり)→ 次曲へ進行 |
| B | hits ≥ 60% |
| C | hits ≥ 40% |
| D | それ以外 |
hits = perfect + good、total = perfect + good + miss(判定済みノーツのみ)。S ランクは紙吹雪 8 枚(8bit LFSR で揺らぎ)+ pulse1 の笛スライド + 両キャラのジャンプループ。
HUD レイアウト
| 項目 | ネームテーブル | 位置 |
|---|---|---|
SCORE: + 6 桁 | $2064 / $206A | row 3 col 4 / 10 |
AUD: + 2 桁 | $2075 / $2079 | row 3 col 21 / 25 |
MODE: TUTORIAL / DANCE | $20A4 / $20B4 | row 5 |
RANK: X | $20EC | row 7 col 12 |
JUDGE: + 7 文字 | $2124 / $212B | row 9 |
| CLEAR プロンプト | $2165 | row 11 col 5 |
GAME OVER メニュー(GAME OVER / CONTINUE / TITLE) | $20E4 / $2124 / $2164 | row 7 / 9 / 11 col 4(24 タイル全幅) |
タイトルメニュー(START / CONTINUE→STAGE SELECT / PROLOGUE / MODE: 行) | $22C4 / $2304 / $2344 / $2384 | row 22 / 24 / 26 / 28 col 4(絵の下、行 29 はオーバースキャン回避) |
LETS DANCE / PAUSE バナー | $216B | row 11 col 11 |
キャラアニメーション
- 全振り共通: 4 コマ ×
ANIM_STEP_LEN(5) = 20 フレーム固定・接地固定(ジャンプのみpose_dyで Y 浮上、ユーザー確定要件)。入力優先順位は L → R → U → D → A → B - アイ(26 フレーム + ゲームオーバー 2 フレーム)とコーチ(32 フレーム)は独立したアニメ状態を持ち、汎用メタスプライト描画で共通描画
- コーチ CHR ストリーミング: 講師 CHR(4,096B、32 フレーム × 128B)を BANK2 から CHR-RAM の $1E00(タイル $E0-$E7)/ $1E80($E8-$EF)へダブルバッファ転送。RAM 上に展開した
lda abs / sta PPUDATA× 64 組(385B)を NMI が実行し、1 vblank 64B × 2 回で 1 フレーム分。位相 0〜3 で管理し、完了時にcoach_tile_baseとcoach_visible_poseを同時に publish して絵と座標のズレを防ぐ - ゲームオーバー中は 32 フレームごとに専用ポーズを交互(アイ 26/27、コーチ 30/31)
サウンド(src/sound.s)
Web トラッカー(tools/nes_tracker_template.html)の再生セマンティクスを 1:1 移植した 5ch ドライバ。
| 項目 | 内容 |
|---|---|
| チャンネル | pulse1 / pulse2 / triangle / noise / DPCM(kick / snare / tom の 3 サンプル、固定バンク 64B アライン) |
| テンポ | 8.8 固定小数タイマ。1 パターン 64 行、4 行/拍 → 16 拍/パターン。オーダー末尾でループ |
| 曲データ | tools/export_bgm_inc.py が assets/bgm/*.json から生成 → BANK0。ROM 収録は DPCM 版 5 曲(JSON は 15 曲) |
| API | sound_init / sound_play_song(A=曲番号) / sound_stop / snd_silence / sound_tick(NMI、PPU 処理後) |
| 同期 | 拍ターゲット表と同一の丸め。カウントイン 96f ちょうどで sound_play_song(tests/chart_sync_test.lua で検証) |
| ポーズ | pause_flag で sound_tick を止め、行位置を凍結 |
効果音
| SE | ch | 条件 |
|---|---|---|
| メトロノーム(880Hz) | pulse1 | 譜面の各拍。BGM 再生中は無効(= チュートリアル専用) |
| セリフ声(周期 170/200 交互) | pulse1 | 会話 1 行につき 2f × 2 バースト |
| 歓声(ノイズ周期 3-6 揺らし) | noise | Lv4 の A/B PERFECT、12f。sound_tick 後に上書き |
| S ランク笛(timer $040→$12C スライド) | pulse1 | CLEAR 画面、60f 周期 |
| 曲選択プレビュー | BGM | カーソル移動ごとに sound_play_song |
判定音(PERFECT/GOOD/MISS)と決定音は未実装。
一枚絵の表示(プロローグ #79 / タイトル #86)
- 絵 = パターンテーブル 1($1000)、文字 = テーブル 0。行 19 に置いた sprite 0(絵と同じタイル・BG 優先)の hit(scanline 152)から約 11 走査線待って PPUCTRL bit4 を 0 に切替(固定バンクの
picture_split_wait、両シーン共用)。絵のタイル 0 は黒固定で行 20 はどちらのテーブルでも黒になるため、切替位置の許容幅が広い。NMI はppu_ctrl_value(絵の表示中 %10011000)を書き戻す - プロローグのデータは
tools/export_prologue_pictures.pyがassets/prologue/nes/pN.*から生成するsrc/prologue_pictures.inc(P1/P2 = BANK5、P3/P4 = BANK6、記述子表は固定バンク)。文字色はページのパレットで最も明るい色を選び、フォント転送時にその色番号のプレーンへ複製 - どちらのシーンも
@actors(アイ・コーチ描画、コーチ CHR ストリーミング)を通らない。プロローグのシーン処理は BANK4(DEBUG は BANK3)に常駐
タイトルの絵と呼吸アニメ(#86)
- データは
tools/export_title_picture.pyがassets/title/nes/title.*(make title-nesの出力)から生成するsrc/title_picture.inc(BANK0)。reset のENTRY_TITLE(production のみ)でtitle_pictureフラグを立て、BANK0 のtitle_load_pictureが CHR $1000 / BG パレット / NAM 行 0〜19 + 黒行 / ATR / sprite 0 を構築する。DEBUG ビルドはタイトル画面を持たない(debug_dispatchが全フレームを奪う)ので、この経路を通らずスプライトシートを載せたまま - パレット: P0 ロゴ
0f,27,15,38/ P1 顔・手0f,0c,36,21/ P2 服0f,0c,30,21/ P3 ポータル0f,21,25,35。メニュー文字は color 1 が最も明るい P0(属性 $00) - 呼吸 = パレット書換のみ(CHR/NAM 不変)。
title_breathe_palettes(16B × 8 ステップ、エクスポータが生成)を 12f ごとに 1 ステップ進め、[15, $3F, $01, 15B]を ppu_buf に積む($3F00 は書かないので隠しコマンドの白フラッシュと衝突しない)。ステップの輝度行オフセットは −1/0/0/+1/+1/0/0/−1、行 3 を超える色は白 $30。呼吸するのは P0 の 3 色・P3 の 3 色・P1/P2 が共有する $21 で、肌 $36 と服 $30 は固定 - STAGE SELECT へは reset(
ENTRY_SONG_SELECT)で入る: 絵が $1000 を占有しておりステージ BG・スプライトが無いため、その場でflow_stateを切り替えられない
主要 RAM(zeropage・抜粋)
| addr | 変数 | 役割 |
|---|---|---|
| $0B | flow_state | フロー状態機械 |
| $0C-$11 | progress_* | 進行レコード(上記) |
| $27/$28 | song_frame_lo/hi | 16bit ノーツレーンクロック |
| $29-$2C | next_note_index / next_note_target_* / chart_finished | 判定カーソル |
| $2D-$2F | coach_chart_pos / coach_target_* | コーチ専用譜面カーソル(96f 先行) |
| $30-$35 | perfect_cnt / miss_cnt / target_cnt / total_* | フレーズ集計・累計 |
| $36 / $39 | clear_rank / audience | ランク / 観客 0〜16 |
| $41-$47 | cptr / scan_silent / restart_pending / tptrl / tptrh | 現在曲の譜面・ターゲット表ポインタ |
| $49-$4B | pose / coach_pose / pose_timer | アニメ状態 |
| $65-$67 | pause_flag / clear_pending / audience_queue_pending | フレーム分離フラグ |
| $78-$7A | title_picture / title_breathe_step / title_breathe_timer | タイトルの絵フラグ・呼吸ステップ(サウンド ZP の後ろに追加 #86。DEBUG は $82-$84) |
| $05E7- | snd_* | サウンドドライバ状態(snd_playing $05E7 / snd_song $05E8 / snd_ordpos $05EF / snd_row $05F0) |
ZP 変数を追加・削除したら Lua テストのアドレス定数を build/engine.dbg から引き直す(実アドレスは dbg が正)。
デバッグ ROM(make debug)
ca65 -D DEBUG_MENU=1。BANK3 に 9 項目のメニュー: DANCE 6 MOVES LOOP / JUDGE P G MISS / AUDIENCE LV1-4 / LV4 FLASH AND CHEER / DIALOG 3 PAGES / OPENING ENTRANCE / BGM ALL SONGS SELECT / CLEAR SCREEN(常に RANK S)/ GAME OVER SCREEN。B または START でメニューへ戻る(warm-reset 署名 $A5/$5A で選択を保持)。
ビルド・実行
bash
make # build/engine.nes(131,088B)
make debug # build/engine-debug.nes(DEBUG_MENU)
make nromtest # build/nrom_test.nes(NROM 32KB+8KB、ドナー基板のスモークテスト)
make flashbin # build/flash/prg_512k.bin / chr_512k.bin(SST39SF040 書き込み像)
make run # Mesen2 で起動(要 MESEN 環境変数)
python3 -m unittest discover -s tools -p 'test_*.py' # 73 tests詳細はビルド手順。実機化は実機 ROM 化ガイド→フラッシュライター→カート改造。