AIドパガキ GBC 版 issue 分割案
本書は GBC 版の issue 分割・順序・依存関係・受入条件の正典である。issue の粒度や受入条件が他文書と食い違った場合、本書を正とする。
⚠ GitHub 番号との対応: 本書の親
#Pは GitHub #1、本書の子#Nは GitHub #N+1(例: 本書 #2 ビルド基盤 = GitHub #3)。GitHub 上の相互参照は各 issue 本文を正とする。
前提: 1 issue = 1 PR。実装者は Codex。ブランチ命名は NES 版踏襲で feature/issue-<N>-<slug>。 参照: ../spec/aidopagaki-gb-design.md / architecture.md / assets.md / e2e.md
0. 全体方針
| 方針 | 内容 |
|---|---|
| 検証を先に置く | E2E ハーネスを機能実装より前(子 2)に作り、「同一入力 2 回でスクリーンショットがバイト一致」を最初に証明する。これが通らないうちは他のテストを書かない |
| 受入条件は機械検証可能なものだけ | 「きれいに見える」は受入条件にしない。人間の目でしか判定できないものはゴールデン画像の人間承認という形で明示的に分ける |
| 予算表を実測で更新する | 各 issue の受入条件に「architecture.md の予算表を実測値で更新する」を入れ、hQHiwater の実測を PR 本文に貼る(NES 版「コミット本文が検証レポート」の継承) |
| コミット規約 | <type>: <日本語要約> (#<issue>)。type は feat: fix: docs: art: chore:。art: はアセット作業専用の一級 type として残す |
| ビルド確認 | make と make debug の両方を exit code で確認する(NES 版 鉄則 #6) |
1. 親 issue
#P AIドパガキ Game Boy Color 版(親)
ゴール: NES 版「AIドパガキ」を GBC 実機で動く ROM に移植する。ゲーム仕様(判定・スコア・観客・ランク・進行・テキスト)は原作を維持し、ハードウェア差分(画面 20×18、DPCM 消滅、VRAM 2 バンク、GDMA、倍速)に対する再設計だけを行う。加えてユーザー要望により一枚絵 5 枚(タイトル 1 + プロローグ 4)を新規に追加する。
完了の定義:
- タイトルからエンディングまで通しでプレイでき、
make e2eが exit 0 - 5 曲すべてで
ref_sim_gb.pyと 1,500 フレームのバイト一致 - ROM 128 KB、CGB 専用、セーブなし
- 設計書 4 本 + ビルド手順 + カートリッジ改造手順書が揃っている
子 issue: 16 本(下記)
2. 子 issue 一覧と順序
| 順 | # | タイトル | 依存 |
|---|---|---|---|
| 1 | #1 | 設計書一式の整備(仕様 / アーキテクチャ / アセット / E2E / issue 分割) | — |
| 2 | #2 | ビルド基盤(RGBDS 1.0.3 + Hello GBC + Makefile)+ PyBoy E2E ハーネス | #1 |
| 3 | #3 | アイ 26F スプライト(2bpp 変換 + 8×16 + 帯 REMAP + 描画 + 6 振りポーズ) | #2 |
| 4 | #4 | ステージ BG(20×18 レイアウト)+ パレット 8 本 + 観客 2 段 32 席 + 視聴率ゲージ | #2 |
| 5 | #5 | リズムコア(レーン / 判定 / スコア / 観客集計、メトロノームのみで BGM なし) | #3, #4 |
| 6 | #6 | APU ドライバ + ドラム再合成 + ref_sim_gb.py | #2 |
| 7 | #7 | 譜面 4 曲統合 + カウントイン同期 + 曲進行 + EASY モード | #5, #6 |
| 8 | #8 | 講師 32F スプライト(VRAM bank1 常駐)+ 入場演出 | #3 |
| 9 | #9 | かなフォント + テキストエンジン + 会話 3 ページ + チュートリアル + Lv4 ハイプ | #4, #8, #5 |
| 10 | #10 | CLEAR / RANK D〜S / ゲームオーバー + S ランク祝賀 + ポーズ + 裏技 | #7, #9 |
| 11 | #11 | タイトル一枚絵 + メニュー + アトラクト | #10, #9 |
| 12 | #12 | プロローグ 4 枚一枚絵 + ページ送り | #11 |
| 13 | #13 | エンディング 12 ページ + 曲 4 同時再生 | #10, #9 |
| 14 | #14 | デバッグメニュー ROM(12 項目) | #10 |
| 15 | #15 | GB カートリッジ改造手順書 + フラッシュ書込み手順書 | #2 |
| 16 | #16 | ビルド手順 / README / 説明書 LP / ドキュメントサイト | 全部 |
2.1 依存グラフ
#1 ──▶ #2 ──┬──▶ #3 ──┬──▶ #5 ──┬──▶ #7 ──┬──▶ #10 ──┬──▶ #11 ──▶ #12
│ │ │ │ │
├──▶ #4 ──┘ │ │ ├──▶ #13
│ │ │ │ │
│ └──▶ #8 ──┴──▶ #9 ──┘ └──▶ #14
│
├──▶ #6 ─────────────────────▶ #7
│
└──▶ #15
すべて ──▶ #16クリティカルパス = #1 → #2 → #4 → #5 → #7 → #10 → #11 → #12 → #16 = 9 本。
並行可能:
- #6(サウンド)は #2 の直後から独立に走れる。 #7 で合流するまでグラフィクス系と一切干渉しない
- #15(ハードウェア文書)は #2 の直後から最後まで独立。ROM の完成を待たない
- #3 と #4 は互いに独立(#2 の後)
3. 子 issue の詳細
#1 設計書一式の整備
背景: NES 版は docs/spec/aidopagaki-design.md を「数値の正典」とし、そこに全定数を集約する運用で 71 issue を回してきた。GBC 版でも実装より先に正典を置き、Codex が参照する 1 点を作る。設計判断(倍速常用 / リセット遷移廃止 / 8×16 OBJ の 2 帯化 / GDMA のみ / 一枚絵 5 枚 / セーブなし)はすでに確定しているので、本 issue はそれを文書として固定するだけである。
スコープ In:
docs/spec/aidopagaki-gb-design.md(ゲーム仕様の正典)docs/design/architecture.md/assets.md/e2e.md/issues.mdtools/quantize_gbc.py(一枚絵の減色プロトタイプ)とtools/requirements.txt
スコープ Out: アセンブリのコード、アセット、E2E の実装
⚠ 値生成スクリプト 3 本(gen_palettes.py / gen_noise_table.py / export_charts_gb.py)は本 issue のスコープから外し、後続 issue へ移した。 gen_palettes.py は #4(パレット)、gen_noise_table.py と export_charts_gb.py は #6 / #7(サウンド・譜面)で実装する。本 issue では値そのものを文書に確定させ、後続 issue の受入条件が --check で再計算・突き合わせる形にする。受入条件 2 / 3 / 4 はその移管を反映して「文書側の値が機械検算に耐えること」を求める。
受入条件:
- 5 文書がすべて存在し、各文書の冒頭に「本書は何の正典か」の宣言が 1 行ある
assets.md§1.1 の 15bit hex 24 件と全文書のインライン参照がR | (G<<5) | (B<<10)から再計算した値と全件一致し、全 BG パレットの c1 ==$7FFF(#4 のgen_palettes.py --checkが同じ検算をする)- ノイズ表 16 エントリが
f = 262144/(r × 2^shift)(code 0 → r = 0.5、上限 524,288 Hz)と全件一致(#6 のgen_noise_table.py --checkが同じ検算をする) speed88 = 1911, 1792, 1977, 1509, 2389とtarget[0] == 96が仕様 §7.2 / §7.3 の式から再計算した値と一致(#7 のexport_charts_gb.py --dry-runが同じ検算をする)- 仕様 §12 の定数一覧が
src/constants.incと 1:1 対応する。BG マップアドレス 21 本とCHARTED_SONGS_MASKを含み、AUDIENCE_ANIM_FRAMES/NOTE_SPEED_88の表記が全文書で揃っている - 文書中のパスに
gbサブフォルダが 1 箇所も現れない: §4 のGB_SUBDIR_GREPがdocs/に対して 0 件(exit 1) numpy/Pillowが 1 つも入っていないクリーンなクローンでpython3 tools/quantize_gbc.py --helpが exit 0 で使い方を表示する(import numpy/from PIL import Imageを_load_deps()へ遅延させる。assets.md§6.1)。--in/--out-dir/--name/--cropを受け取り、絶対パスを 1 つも直書きしていない。出力ファイル名がassets.md§5.2 の規約(<name>.png/<name>-preview.png/<name>-report.txt/<name>.gbcpal)と一致するpython3 -m unittest discover -s toolsが依存なしの環境で exit 0(共通受入条件 2)。test_quantize_gbc.pyの減色再現性 3 件はunittest.skipUnlessで skip され、CLI 検査 1 件は依存なしで通る(assets.md§8)
タスク分解: 文書 5 本の執筆 → quantize_gbc.py の argparse 化と出力名の規約合わせ → tools/requirements.txt → 相互リンクの整備 → 数値の機械検算
テスト計画: 文書内の数値表を Python で再計算するスクリプトを回し、出力全文を PR 本文に貼る。quantize_gbc.py --help の exit code を確認する
#2 ビルド基盤 + PyBoy E2E ハーネス
背景: NES 版の最大の弱点は「Mesen2 が非対話起動できず lua スイートが実運用のハーネスにならなかった」ことだった。GBC 版では PyBoy が完全に非対話で動くので、機能実装より前に E2E を作り、決定性を先に証明する。スパイクで RGBDS 1.0.3 + hardware.inc v5.3.0 + PyBoy 2.7 の組合せが動くことは確認済み。
スコープ In:
- RGBDS 1.0.3 の導入手順、
hardware.incv5.3.0 の vendoring Makefile(all/debug/test/e2e/run/clean)- Hello GBC ROM(フレームカウンタ + ボタン検出のみ)
- 倍速モード切替(
rP1=$30→rIE=0→KEY1→STOP) - VBlank ISR + OAM DMA(HRAM スタブ 10 B)+ VRAM キュー 256 B(
q_reset/q_push/q_push_digits/q_hiwater_resetの 4 本)+ GDMA ヘルパ 2 本(gdma_row= VBlank 専用 /gdma_bulk= LCD OFF 専用) tests/harness.py(.sym解決 / peek / poke / press / screenshot /tick()の不変条件)
スコープ Out: ゲームロジック、アセット、サウンド
受入条件:
makeが exit 0 でbuild/aidopagaki.gbcを生成し、$0143 == $C0/$0147 == $19/$0148 == $02。make debugも exit 0rgblink -nが.symを出力し、harness.sym["hFrameCount"]が解決できる。.symに載らないハードウェアレジスタ等はtests/harness.pyのHW定数表に集約し、表の行以外では 16 進 4 桁または 10 進65xxxのアドレスを直書きしない(tools/check_conventions.shの検査対象)。- S0(決定性): 同一入力列を 2 回実行し、300 フレーム目のスクリーンショットがバイト一致。
hFrameCountの全履歴も一致 wait_for_boot()が CGB ブート ROM(約 66-67 フレーム)を越えて 300 フレーム以内に成功する。Init クリアループ込みの実測は 83(2026-09-02、PyBoy 2.7.0)。壊れた ROM ではRuntimeErrorを投げるhCurSpeed == 1かつKEY1bit7 == 1(倍速に入っている)。SameBoy と BGB でも起動することを手動確認し PR 本文にスクリーンショットを貼るq_pushを意図的に溢れさせるとhQOverflowが増え、通常時は 0。hQHiwaterが PyBoy から読め、flush ではリセットされず累積最大が残る(q_hiwater_resetを呼んだときだけ 0 になることをテストで確認する)gdma_rowが VBlank 内で完了し、呼出し時と転送完了時の両方でLYが 144-153 の範囲を出ない(フックの入口と出口で検査。e2e.mdS4 ①)。gdma_bulkは入口の HL / DE / C を記録し、1 ≤ C ≤ 128、src/dst の 16 B 整列、dst + C*16 <= $A000、LCD OFF、入口/完了時の VBK が同一(0 または 1)であることを検査する- LCD を OFF にする全書込みを命令サイトで捕捉し、
wait_for_boot()後の全フレームで、書込み値のrLCDCbit7 = 0 がLY >= 144に限られる(フックで検査)。ON 側は検査しない(LCD OFF 中は VBlank が来ないため、ON を VBlank 中に行うことは原理的に不可能) - HRAM / WRAM のシンボルが
architecture.md§8 の接頭辞規約(HRAM =h、WRAM =w)に従っている make e2eが exit 0
タスク分解: brew での RGBDS 導入確認 → hardware.inc vendoring と定数名の突き合わせ(LCDC_* / LCDC_BLOCK01 等) → リンカスクリプトと 8 バンク定義 → 倍速切替 → ISR → キュー → GDMA → ハーネス → S0
テスト計画: tests/test_boot.py(S0/S1/S2)、test_budget.py(S3/S4)。初回実行は必ず timeout 60 python ... で包む(ei/halt の実機タイミングバグが Python 側のハングに見えるため)
⚠ ei と最初の halt の間に nop を必ず置く。 スパイクで実際に踏んだ罠。
#3 アイ 26F スプライト
背景: NES 版のアイは 26 フレーム × 8 タイル = 208 タイルの手打ちピクセルマップで、「バイト不変が原則」という申し送りがある。GBC では 8×16 OBJ を使うためタイル並びとパレット帯が変わるが、ピクセルデータは 1 ビットも変えない。
スコープ In:
tools/gbgfx.py(2bpp エンコーダ、8×16 ペア並べ替え、REMAP)tools/nes_chr_to_gb.pyとgen_ai_sprite_gb.pytools/test_chr_equivalence.py(ジェネレータ出力 == NES.chrのビット変換)- OBJ パレット OBP0/OBP1、8×16 メタスプライト描画、アイドル往復(半拍 16f)
- 入力読み取り(正規化 + 起動時 1 回読み捨て)と 6 振りのポーズ状態機械(4 コマ × 5f = 20f)
スコープ Out: 講師、ノーツ、判定
受入条件:
assets/ai-26f.2bppが 3,328 B、ai-gameover.2bppが 256 B、再生成でバイト一致、SHA-256 ロックが一致test_chr_equivalence.pyが green(ビットプレーン変換段階で NES.chrと完全一致)GB_TILE_ORDER == (0,2,4,6,1,3,5,7)が順列であること、REMAPの 4 行がすべて全単射であること、再マップ後の各タイルが非ゼロ index を 3 種類以内しか使わないことを unittest が検査- OAM idx 0-3 の 4 OBJ でアイが表示され、
tick()の per-line ≤ 10 が全フレームで成立 - 半拍(16 フレーム)ごとに frame 0/1 が往復する
get_move_inputが ← → ↑ ↓ A B の優先順で 1 つだけ返す。全 6 振りが 20 フレームで 4 コマ再生。ジャンプのみpose_dy = 0,-2,-5,0、第 1 ステップでpose_dxが 2 倍- リセット直後の 1 回読み捨てが入っている(NES issue #69 対策)
⚠ 受入条件 8(人間承認): 帯パレット 3 帯 → 2 帯の統合で下半身の見た目が劣化していないことを、03_ai_poses.png のゴールデンでレビュー担当(Fable)がプレビューを目視承認する。承認されるまで #5 へ進まない。
タスク分解: gbgfx.py → 等価性テスト → gen_ai_sprite_gb.py → OBP0/OBP1 定義 → OAM 構築 → ポーズ状態機械 → 入力
テスト計画: tools/test_gbgfx.py / test_chr_equivalence.py / test_ai_sprite_gb.py、E2E で 6 振りループのスクショ 6 枚
#4 ステージ BG + パレット + 観客 + 視聴率ゲージ
背景: NES の 32×30 を 20×18 に落とす画面設計は全アセットの前提になる。観客テーブル 11,424 B は廃してランタイム合成にし、視聴率ゲージ 16 タイルを新設する。
スコープ In:
tools/gen_stage_bg_gb.py(BG タイル 128 枚 + 各シーンの tilemap/attrmap)tools/gen_ui_font_gb.py(英数 47 タイル)- BG パレット 8 本 +
gen_palettes.py(--check付き) - BG マップ
$9800単一面($9C00は使わない。LCDCbit3 は常に 0) - 観客 2 段 × 16 席(1 席 = 1 タイル幅 × 2 行)のランタイム合成
- 視聴率ゲージ 16 タイル(
gdma_rowで更新) - HUD(
SC:6 桁 /AUD:2 桁 /NRM)
スコープ Out: キャラ、レーン、判定
受入条件:
../spec/aidopagaki-gb-design.md§2.1 の 18 行レイアウトが表示され、スクリーンショットが golden と一致- BG タイル総数 ≤ 128 かつ
max(tilemap) < 128を unittest が検査(assertでビルドを落とす) - 仕様 §12 の BG マップアドレス定数 21 本が
src/constants.incにあり仕様 §2.3 と一致することをASSERTが検査し、どの op も$xx00をまたがないことをASSERTとharness.assert_no_page_cross()の両方が検査 - 全 BG パレットの c1 ==
$7FFF(unittest)。15bit 値が(R,G,B)からの再計算と全件一致。gen_palettes.py --checkが exit 0 - BG マップが
$9800の 1 面だけであること:$9C00-$9FFFに書き込むコードが存在せず(grep)、rLCDCに書かれる値が$87/$95/$05の 3 つだけである(フックで全書込みを記録して検査)。シーン切替は LCD OFF 窓で完結し、切替フレームのスクショが「途中の画面」にならない audienceを 0→16 に注入すると点灯席数がn*2で追随し、bg_rowの期待列と一致。観客テーブルが ROM に存在しないことを.mapファイルの検査で確認- 視聴率ゲージの点灯タイル数が
audienceと一致し、帯域境界(4/5、9/10、12/13)で BGP7 の c2 が変わる hQHiwaterの実測値を PR 本文に貼り、architecture.md§4.4 の予算表を更新する
タスク分解: タイルマップ手打ち → BG タイル生成 → パレット生成 → 属性マップ → 観客合成 → ゲージ → HUD
テスト計画: test_stage_bg_gb.py / test_palettes.py、E2E は S17(ゲージ)とレイアウトのゴールデン
#5 リズムコア(BGM なし)
背景: ゲームの核。BGM を待たずにコアループを完成させるため、チュートリアルのメトロノーム(CH1 ビープ、32f/拍固定)だけで動くところまでを本 issue で作る。サウンドドライバ(#6)とは独立に進む。
スコープ In:
- ノーツレーン描画(
x = 16 + (rem*3)>>1、遅刻クランプ、rem > 96は非表示) - 判定マーカー、ノーツ 6 種のタイル(
gen_notes_gb.py) - 判定(±4/±8)、スコア 6 桁、判定文字とその色(BGP2 書換)
- 4 拍集計と観客増減(+2/+1/+1/−2)、0 でゲームオーバー
- チュートリアル譜面(4 拍に 1 ノーツ × 6 グループ)とメトロノーム
flow_stateの骨格とflow_goto()
スコープ Out: BGM、譜面 4 曲、会話、CLEAR 画面
受入条件:
- ノーツ X が
16 + (rem*3)>>1に一致(rem = 96, 64, 32, 8, 4, 0の 6 点で検算)、遅刻は 16 にクランプ、rem > 96は OAM Y = 0 - 最速曲相当の密度(23.58 f/拍)でレーン上ノーツが 6 枚を超えない。per-line OBJ が全フレーム ≤ 10(OAM X ≥ 168 のノーツも数えて)
- S11(判定窓 9 ケース) が全通過:
target-9,-8,-5,-4,0,+4,+5,+8,+9に対し MISS/GOOD/GOOD/PERFECT/PERFECT/PERFECT/GOOD/GOOD/MISS、スコア差分 0/50/50/100/100/100/50/50/0 - S13(窓外の空振り):
next_note_indexとtotal_*が不変(NES issue #62) - S14: 窓外・誤ボタンでも
poseが押した振りの先頭コマになる - スコアが 999999 でクランプ。チュートリアル中は加算されない
- S16(観客増減 4 パターン) が全通過。0 で
game_state == 1、16 でクランプ - 判定文字の色が PERFECT=金 / GOOD=水色 / MISS=赤 に切り替わり、VRAM キューを 1 バイトも使わない(
hQHiwaterが判定色変更で増えない) ld [wFlowState], aがflow_gotoの中の 1 箇所だけであることをgrepが確認
タスク分解: ノーツタイル → レーン描画 → 判定 → スコア → 判定文字 → 集計 → メトロノーム → フロー骨格
テスト計画: tests/test_judge.py(S11-S15)、test_audience.py(S16)
⚠ 本 issue の完了時に NOTE_SPEED_88 = 1.5 px/f のプレイテストを行う。 見づらければ 64f × 2px へ退避する(定数 2 個 + export_charts_gb.py の再実行)。判断結果を PR 本文に書く。
#6 APU ドライバ + ドラム + ref_sim
背景: 移植で最大の作業量。DPCM が使えないのでドラム 3 種を作り直す。NES 版で音の正しさを担保した唯一の手段が ref_sim.py のビット一致だったので、同じ体制を GBC で再現する。apu_shadow なしでは検証が原理的に成立しない(GB の APU レジスタは読み戻せない)。
スコープ In:
wApuShadow23 B($C310、$xx10境界)+wWaveShadow16 B($C330、$xx30境界、同一ページ)、snd_write_reg1 点経由の規約- ピッチの内部表現(
div = 131072/f、書込み時のみX = 2048 - div) - 周期表 84 エントリ、ノイズ表 16 エントリ、
vol_mul256 B - リトリガ規約(値が変わったフレームだけ
NRx4を書く、CH3 はNR32の 4 段) - ストリーム再生(8.8 タイマ / 楽器 4 系列 / fx 6 種)
- ドラム 3 種のフレーム表 128 B と CH3 借用の調停、
-D DRUM_USE_WAVE=0/1 tools/ref_sim_gb.py、WRAM リングログ(-D DEBUG=1)- エクスポータ
export_bgm_gb.py(下限周波数の警告 + vol 系列の平坦化)
スコープ Out: 譜面との同期(#7)、SE(#10)
受入条件:
grep -n 'ld \[\$FF[123][0-9A-F]\], a' src/がsnd_write_regの中の 1 件だけ(APU 直書き禁止の機械検証)- シャドウの境界整列:
ASSERT LOW(wApuShadow) == $10/ASSERT LOW(wWaveShadow) == $30/ASSERT HIGH(wApuShadow) == HIGH(wWaveShadow)がビルド時に通る。snd_write_regがld h, HIGH(wApuShadow)/ld l, cの 2 命令で両シャドウを振り分け、$FF30-$FF3F用の分岐コードを持たない。wApuShadow23 B が PyBoy から読め、読み戻し可能なレジスタ($FF10$FF11$FF12$FF16… )についてはレジスタ別の読戻しマスクを適用したうえで実 APU の読み値と一致する。シャドウの書込先がチャンネル状態(wChanState = $C340-$C37F)と 1 バイトも重ならない divの値域検査: CH1/CH2 は1 <= div <= 2047、CH3 は1 <= div <= 4095。MIDI 26(div = 3571)のベースが鳴ること。CH1/CH2 で MIDI ≤ 35、CH3 で MIDI ≤ 23 のノートに対しエクスポータが警告を出し 1 オクターブ上げる- ノイズ表 16 エントリが
f = 262144/(r × 2^shift)(code 0 → r=0.5、上限 524,288 Hz)で再計算した値と一致。gen_noise_table.py --checkが exit 0 - ドラム表が 128 B(kick 12f / snare 9f / tom 11f)で、
architecture.md§7.7 の 4 B ビット割当(B0/B1 = CH3 X 11bit +NR322bit +CH3_WRITE/CH4_WRITE/CH3_RELEASEの 3 フラグ、B2 =NR42、B3 =NR43)に従う。ch3_div = round(65536/f)からCH3 X = 2048 - ch3_divが全フレームで一致。各ドラムの最終 CH4 フレームがCH4_WRITE = 1かつNR42 = $00(DAC オフによる停止。長さカウンタに依存しない)、CH3_RELEASEが各ドラムでちょうど 1 フレームだけ立ち、その位置が KICK = f11 / SNARE = f2 / TOM = f10 である(SNARE だけ最終フレーム f8 ではない。CH3 ボディが f0-f1 で終わるため。最終フレームに置くとベースが 6 フレーム余分に途切れる) ref_sim_gb.pyと 1,500 フレーム分の書込みイベント列(frame, reg_lo, value)が完全一致(最低 1 曲、DRUM_USE_WAVEの両モード)。イベントの本数も一致すること(同値NRx4の余計なリトリガを検出するため。シャドウの値スナップショット比較では検出できない)- wave RAM は
sound_initの 1 回だけ書かれる(書込み回数カウンタで検査) - fx
0xy1xx2xx3xx4xyVxxが NES 版と同じ符号方向で動く(ref sim が検出) ref_sim_gb.pyに絶対パスが 1 つも書かれていない(unittest)
⚠ 受入条件 10(人間承認): CH1/CH2 の毎フレーム音量更新でリトリガによる duty 位相リセットが耳障りでないことを実機または SameBoy で試聴確認する。駄目なら R3(vol 系列の定常部平坦化)を強める。本 issue の最大リスクなので早期に確認する。
タスク分解: apu_shadow → 周期/ノイズ/vol 表 → チャンネル更新 → ストリーム → fx → ドラム表 → 調停 → ref sim → リングログ → エクスポータ
テスト計画: test_period_table.py / test_noise_table.py / test_drum_tables.py / test_export_bgm_gb.py / tests/test_apu_refsim.py
#7 譜面 4 曲統合 + 進行 + EASY
背景: charts.json と BGM JSON は NES 版と同一ファイルをそのまま使う。変わるのは speed88 だけ。song_frame == 96 での BGM 開始は NES issue #35 が担保していた唯一の同期アサーションであり、これを温存する。
スコープ In:
export_charts_gb.py(GB_FPS、NOTE_APPROACH_FRAMES = 96)- 曲と譜面を同一バンク(bank1)に置く不変条件
- カウントイン 96 フレーム →
sound_play_song - コーチの独立譜面カーソル(
coach_chart_pos) - 曲進行(RANK A 以上でクリア扱い、B 以下でリトライ)
- EASY モード(偶数拍の休符化、
$FF終端は除外) - 本番中 SELECT の曲やり直し(
restart_pending)
スコープ Out: CLEAR 画面(#10)
受入条件:
speed88が 1911 / 1792 / 1977 / 1509 / 2389、target[0] == 96、単調増加、$FF終端、CHARTED_SONGS_MASK == %00001111- S6:
snd_playingが 0→1 になるフレームでsong_frame == 96ちょうど(±0) - S7: 拍 index
Nからtarget[N] = 96 + ceil(N * 4 * speed88 / 256)を再計算し、ROM のtarget[]が全曲・全拍で一致する。coach_target/next_note_targetがどちらもこのtarget[]の要素である。⚠ 「差分が4*speed88/256の整数倍」という検査にしてはならない(ceilにより拍間隔が揺れるので 5 曲中 4 曲で必ず落ちる) - 5 曲すべてで
ref_sim_gb.pyと 1,500 フレームのイベント列一致。5 曲の実時間が仕様 §7.2 の表と ±0.05 s 以内 - 曲と譜面が同一バンクにあることを
.mapファイルの検査で確認 - S20(EASY):
easy_mode == 1で偶数 index の拍がすべて REST。$FF終端は残り、譜面が正常終了する - S9(曲進行): RANK A 以上で
cleared_maskのビットが立ち次曲へ、B 以下で同一曲リトライ。曲 4 は選曲・進行から除外される - 本番中 SELECT で次の曲に切り替わり、再カウントインが 96 フレームで走る。
hQHiwaterが 128 を超えない
タスク分解: export_charts_gb.py → バンク配置 → カウントイン → コーチカーソル → 進行 → EASY → SELECT
テスト計画: test_export_charts_gb.py、tests/test_flow.py(S6/S7/S9)、test_easy.py(S20)
#8 講師 32F スプライト + 入場演出
背景: NES 版で最大の複雑性だった「講師 CHR の自己書換えストリーミング」(coach_stream_code 385 B + 4 状態 + NMI 内 599 cycle)を全廃する。GBC では VRAM bank1 に 32 フレーム 4,096 B が丸ごと常駐できる。移植で最も単純化される部分。
スコープ In:
gen_coach_sprite_gb.py(32 フレーム、bank1 常駐)- OBJ 属性 bit3 = 1 による bank1 タイルの参照
- OBP2 / OBP3
- 入場演出(コーチが歩いて入ってくる、フレーム 26-29)
- コーチのポーズ状態機械(アイと隣接配置し
ACTOR_IDでインデックス)
スコープ Out: 会話(#9)
受入条件:
assets/coach-32f.2bppが 4,096 B、32 フレーム、bank1 にちょうど収まる(256/256 タイル)- OBJ 属性 bit3 = 1 で bank1 のタイルが引かれ、アイ(bank0)と同時に正しく表示される
- 自己書換えストリーミングのコードが 1 行も存在しない(
grep -c 'coach_stream' src/が 0) - アイと同一 Y 帯で per-line OBJ ≤ 10(実測 4 枚)
- 入場演出で歩行 4 コマ(26-29)が再生され、
COACH_Xが 0 から 48 まで移動して停止する - CGB の OBJ 優先度が OAM 順であることに従い、アイ(OAM 0-3)がコーチ(OAM 4-7)の前に描かれる。重なりのスクショが golden と一致
タスク分解: 講師 CHR 変換 → bank1 ロード → OBP2/3 → 描画 → 歩行 → 入場フロー
テスト計画: test_coach_sprite_gb.py、E2E で入場のスクショ
#9 かなフォント + 会話 + チュートリアル + Lv4
背景: 美咲ゴシック BDF がリポジトリ外にあり手元にない。NES 版が既に生成済みの .chr(会話 54 / プロローグ 60 / エンディング 76)と JSON マッピングをビットプレーン変換して流用する。テキストは 1 文字も変えず、TIKTOK → SNS の 1 箇所だけを置換する。
スコープ In:
gen_kana_font_gb.py(NES.chr3 本 → GB 2bpp)- かな窓の GDMA 入替(会話 54 / プロローグ 60 / エンディング 76)
- BG 属性 bit3 による英数・かなの行内混在
reflow_text.pyと逐語 unittest- テキストエンジン(8 フレームに 1 行、1 フレーム 1 行だけキューに積む)
- 会話 3 ページ、チュートリアル 6 ノーツの完成、Lv4 ハイプ演出
スコープ Out: プロローグ(#12)、エンディング(#13)
受入条件:
- かな 3 種のタイル数が 54 / 60 / 76、すべての使用文字にグリフがある(未定義文字の化けを防ぐ。NES 申し送りの罠)
- 全 50 行(プロローグ 12 + 会話 8 + エンディング 30)が 20 タイル以内(最長 18)、1 ページ 4 行以内。
TIKTOKが ROM 内に存在せずSNSが存在する。⚠ 行数と最長タイル数はtest_text_pages.pyが毎回再計算し、その出力を正とする - 逐語一致テストが green(NES 原文をテストに書き込み、
TIKTOK → SNSの 1 箇所以外は完全一致。コピー承認ゲート) - かな窓の入替が LCD OFF 窓で完結し、
hQHiwaterが 128 を超えない。flow_enter_endingがエンディングかなの入替を呼ぶことを静的解析テストが確認 - S26: 全ページで
bg_rowのタイル列がreflow_text.pyの期待列と完全一致。8 フレームに 1 行ずつ現れる - S25: チュートリアル譜面が 4 拍に 1 ノーツ × 6 グループで L→R→U→D→A→B。SELECT で最終セリフへスキップ。メトロノーム(880 Hz / 8f)が
apu_shadowに出る - S18(Lv4 ハイプ):
audience >= 13で A/B を PERFECT → BGP1/BGP3 が 2 フレームだけ$7FFF、CH4 に 12 フレームの歓声。パレットシャドウから正しく復帰する - S5(フル導線):
flow_stateが 9 → 8 → 0 → 1 → 4 → 2 → 3 → 5 → 6 の順に遷移
タスク分解: かな変換 → 窓入替 → 属性混在 → reflow_text.py → 逐語テスト → テキストエンジン → 会話 → チュートリアル → Lv4
テスト計画: test_kana_font_gb.py / test_text_pages.py、tests/test_text.py(S25/S26)、test_audience.py(S18)
#10 CLEAR / RANK / ゲームオーバー + S 祝賀 + ポーズ + 裏技
背景: 完走体験と開発用の操作をまとめて入れる。ランク閾値テストはリセット遷移を廃止したからこそ状態注入で書ける(NES では lua で debug ROM を経由するしかなかった)。
スコープ In:
- ランク算出(割り算なし、16bit 乗算 + 比較)
- CLEAR 画面(
RANK: x+ バナー 4 種) - ゲームオーバー画面(アイ 26/27、講師 30/31 の交互)
- S ランク祝賀(ジャンプ永久ループ、紙吹雪 8 枚 + LFSR、ホイッスル SE)
- ポーズ(
sound_gateで BGM を行位置ごと凍結) - 裏技 3 種(
A+B+SELECT強制 S /START+A+Bタイトル復帰 / タイトルのコマンド入力)
スコープ Out: エンディング(#13)
受入条件:
- S19(ランク閾値 5 ケース):
(6,0,10)→D(7,0,9)→C(10,0,6)→B(15,1,0)→A(16,0,0)→S - ランク算出に除算ヘルパが使われていない(
grepで確認) - バナー文字列がすべて 20 タイル以内(
GAME OVER START16 /RETRY - START13 /NEXT SONG - START17 /ALL CLEAR - START17) - S23: 紙吹雪 8 枚が OAM idx 8-15 に存在し Y が単調増加。
confetti_lfsrがx^8+x^6+x^5+x^4+1で周期 255。ホイッスル SE が 60 フレーム周期で書込みイベント列に現れる。CLEAR 中の OAM 使用が 16 枠(アイ 4 + コーチ 4 + 紙吹雪 8)で、同一の 16 px 帯に紙吹雪が 3 枚以上入らない(architecture.md§4.1 の不変条件)。per-line OBJ は全フレーム ≤ 10 - S24: ゲームオーバーでアイ pose 26/27、講師 30/31 が交互になる
- S21(ポーズ): START で
pause_flag == 1、snd_rowが凍結、バナーがPAUSE。再 START でsnd_rowが進む。ポーズ解除フレームのhQHiwaterが 128 以下。消音がNR51 = 0だけで行われ、snd_silence(NR52 = 0)がポーズ経路から呼ばれない(grepと書込みイベント列の両方で確認) - S22(裏技 3 種) がすべて動作する
- CLEAR フレームの
hQHiwater実測値を PR 本文に貼り予算表(設計値 67 B)を更新する
タスク分解: ランク → CLEAR 画面 → GO 画面 → 紙吹雪 → ホイッスル → ポーズ → 裏技
テスト計画: tests/test_rank.py(S19/S23)、test_cheats.py(S21/S22)
#11 タイトル一枚絵 + メニュー + アトラクト
背景: NES 版にはタイトルの一枚絵がなく、GBC 版で新規に追加する。「自動画像変換は禁止」という NES 版の確定ルールと折り合いをつけるため、減色はオフラインで 1 回だけ行い、32 色インデックス PNG を正典としてコミットする。ビルドが触るのは rgbgfx -c gbc:<name>.gbcpal だけになり、決定的になる。
スコープ In:
tools/quantize_gbc.py(オフライン。Makefile から呼ばれない)で 7 パレット量子化(BGP7 はテキスト帯用に固定予約)- ART シーン構成(
LCDC = $95、OBJ OFF、LCDC.4 = 1) - ART のテキスト系タイルを VRAM bank1 に置く(UI 英数 47 + 区切り線 1 + かな ≤60 = ≤108 タイル)。BG 属性 bit3 = 1 で索引。bank0 は一枚絵 240 タイル専用
- 一枚絵の tilemap / attrmap(各 240 B)を CPU が 12 行 × 20 B で
$9800 + row*32へコピーする(GDMA では運べない) - タイトル一枚絵 1 枚 + メニュー 3 項目 +
MODE:NORMAL - EASY 切替(← / → / SELECT)
- アトラクト(600 フレーム無操作 → プロローグ)
- ART ↔ GAME の CHR 往復(LCD OFF + 分割 GDMA)
- パレットフェード(16 段)
スコープ Out: プロローグの絵(#12)
受入条件:
assets/art/title.pngが 160×96 のインデックス PNG、PLTE ≤ 32 色、1 タイルあたり ≤ 4 色、絵が使うパレットが 0-6 の 7 本以内(BGP7 はテキスト帯用の固定色)rgbgfx -c gbc:assets/art/title.gbcpal -u -m -n 8 -s 4 -N 240 -b 0が exit 0 で通り、.2bppが ≤ 3,840 B、.tilemap/.attrmapが各 240 B(32 列へのパディングはしない)。⚠ 画像サイズ 160×96 は-N 240では検出できない(-Nはユニークタイル数の上限であってセル数ではない)ので、test_art_assets.pyの PNG 160×96 検査と.tilemap240 B 検査、およびtest_quantize_gbc.pyの出力 160×96 検査で受け持つ(assets.md§5.3 / §5.4)quantize_gbc.pyが Makefile の依存に現れない(grepで確認)。ビルドは PNG と.gbcpalからのみ走る- S27: ART シーンで
LCDC == $95。BG マップ row 0-11 が絵、row 13-15 がメニュー、row 17 が MODE。row 12-17 のタイルの BG 属性 bit3 が 1(テキストが bank1 から引かれている) - メニュー 4 行のユニークグリフ 16 種(
Hを含まない)+ カーソル▶がすべて bank1 の UI 英数フォント 47 から引かれている(bank0 に英数タイルを置かない)。bg_row(13,0,20)〜bg_row(17,0,20)のタイル列と属性列が期待列と完全一致 - ART → GAME 復帰後にアイと講師の OBJ CHR が正しく再ロードされている(復帰直後のスクショが
03_ai_poses.pngの該当部分と一致) - 分割 GDMA が**1 転送 1-128 ブロック、src/dst とも 16 B 整列、
dst + blocks*16 <= $A000**で、VRAM バンクを跨がない(入口の CPU レジスタと転送完了時の VBK をフックで記録して検査) - START / CONTINUE / PROLOGUE の 3 項目が動く。← → SELECT で NORMAL ⇄ EASY
- S8(アトラクト): 600 フレーム無操作で
wFlowState == 10 - LCD の OFF が VBlank 中(
LY >= 144)にのみ行われる(フックで検査)。⚠ ON 側は検査対象にしない: LCD OFF 中はLYが 0 で停止して VBlank も STAT 割込みも来ないため、「ON を VBlank 中に行う」ことは原理的に不可能で、待ちループを書くと永久にハングする(architecture.md§5.4)
⚠ 受入条件 11(人間承認): assets/art/title-preview.png(4 倍拡大)をレビュー担当(Fable)がプレビューを目視承認する。減色ノイズが許容範囲であることの判断は機械にはできない。承認されるまで #12 へ進まない。
LCD OFF の検査は InitLcdOff ラベルではなく、全経路が通る rLCDC 書込み命令をフックし、wait_for_boot() 後の全フレームを対象にする(ON 側は検査しない)。
タスク分解: quantize_gbc.py → 減色 → 人間レビュー → ART VRAM 構成 → CHR 往復 → メニュー → EASY → アトラクト → フェード
テスト計画: test_art_assets.py、test_quantize_gbc.py、tests/test_art.py(S27)
#12 プロローグ 4 枚一枚絵
背景: NES 版のプロローグはビジョン内のテキスト演出だけで、一枚絵は存在しない(原典 §1.3 に逐語で明記)。GBC 版で 4 枚の絵を追加するのはユーザーの明示的な要望による意図的な原作逸脱である。テキスト演出そのもの(逐語の本文、8 フレームに 1 行、240 フレーム自動送り)は原典どおり維持する。
スコープ In:
- プロローグ 4 枚の減色(
quantize_gbc.py、オフライン) - ページごとの一枚絵差し替え(フェードアウト → LCD OFF → GDMA → フェードイン)
- 下部 3 行のかなテキスト(ART レイアウト、
ART_TEXT_0..2_ADDR) - 手動 A 送り / アトラクト時の 240 フレーム自動送り
スコープ Out: エンディング
受入条件:
- 4 枚すべてが
-N 240を通り、各.2bppが ≤ 3,840 B、.tilemap/.attrmapが各 240 B、PLTE ≤ 32 色、絵が使うパレットが 0-6 の 7 本以内 - 4 ページ × 3 行が 20 タイル以内で表示され、
bg_rowのタイル列が期待列と完全一致 - テキストは 8 フレームに 1 行で現れる(1 ページ = 24 フレームで表示完了)。タイプライタ演出ではない
- A で次ページへ進む。アトラクト時は 240 フレームで自動送りされ、1 ページを送り切る前に次ページへ行かない(3 行 × 8f = 24f << 240f)
TIKTOKが ROM 内に存在しない。プロローグ 1-2 がいつものように SNSをである(逐語テスト)- スクショ 4 枚が golden 一致
- bank6 / bank7 に 5 枚すべてが収まる(1 枚 4,384 B → bank6 13,152 B / bank7 8,768 B + cold code。
.mapの検査。超えたら$0148 = $03へ上げる判断を PR 本文に書く) - ページ遷移中に
tick()の不変条件が破れない
⚠ 受入条件 9(人間承認): 4 枚の -preview.png をレビュー担当(Fable)がプレビューを目視承認する。
タスク分解: 元画のクロップ → 減色 4 枚 → 人間レビュー → ページ差し替え → フェード → 送り → アトラクト
テスト計画: test_art_assets.py(5 枚に拡張)、test_quantize_gbc.py、tests/test_art.py
#13 エンディング 12 ページ + 曲 4
背景: 全曲クリア後のご褒美。エンディングかな 76 タイルは会話 54 と合わせると 130 > 128 でかな窓に入らないため、シーン入場での入替が必須になる。曲 4(トワイライト・スターズ)は譜面を持たずエンディング専用。
スコープ In:
- エンディングかな 76 タイルの窓入替
- 12 ページのテキスト進行(ビジョン内、一枚絵ではない)
- 曲 4 の同時再生
- 12 ページ後のタイトル復帰
スコープ Out: 曲 4 の譜面作成(出荷後)
受入条件:
- 12 ページの本文が逐語一致(unittest がコピー承認ゲート)。全行 20 タイル以内
- エンディングかな 76 タイルが窓に収まり、
flow_enter_endingが入替を呼ぶことを静的解析テストが確認 - 曲 4 が同時再生され
ref_sim_gb.pyと 1,500 フレーム一致 - S10:
cleared_mask == %00001111で CONTINUE が SONG SELECT に変わる。CLEAR で START →flow_state == 12→ 12 ページ →flow_state == 9 - 12 ページ通しで
tick()の不変条件が破れない 50_ending_p12.png(THE END)が golden 一致
タスク分解: かな窓入替 → ページ進行 → 曲 4 再生 → タイトル復帰
テスト計画: test_text_pages.py、tests/test_flow.py(S10)
#14 デバッグメニュー ROM
背景: NES 版は make debug の 9 項目メニューが検証を大きく楽にした。GBC 版は 12 項目に拡張し、特に予算実測(VRAM QUEUE STRESS)と通常速フォールバックの確認(SPEED TOGGLE)を機械では取れない形で補う。
スコープ In:
-D DEBUG=1 -D DEBUG_MENU=1、bank7- 12 項目(NES の 9 + GBC 固有の 3)
- 項目選択後の復帰は
flow_goto()(リセットではない) - APU リングログ(
$C600)の有効化
スコープ Out: 製品 ROM への影響
受入条件:
make debugが exit 0 で 12 項目メニューを表示する- 各項目が対応する演出へ遷移し、B / START でメニューへ戻る(12 項目すべてを E2E で巡回)
- 10 VRAM QUEUE STRESS が
hQHiwaterの実測値を画面に表示し、その値を PR 本文に貼って予算表を更新する - 12 SPEED TOGGLE で通常速に落とし、VBlank に流す GDMA を
gdma_row× 2(64 B)までに抑えた区間を完走してhQOverflow == 0(architecture.md§4.5 の通常フレーム 102.2% を前提に、通常速フォールバックではキュー/GDMA量を追加制限・分割して完走させる)。⚠ 通常速で VBlank GDMA 上限 1,024 B を撃つと VBlank を 135.5% 超過して破綻する。 このモードでは GDMA 上限を 32 ブロック/フレームに落として 2 フレームに分割すること。通常速の 102.2% は通常フレーム(GDMA なし)を含む現在の予算値であり、全フレームを無制限に流せるという意味ではない - 11 PALETTE VIEWER が BG 8 本 / OBJ 8 本を表示する
- 製品 ROM(
make)に DEBUG コードが 1 バイトも入らない(.mapのサイズ比較で確認)
タスク分解: メニュー UI → 12 項目 → リングログ → 速度トグル → パレットビューア
テスト計画: tests/test_debug_menu.py(debug ROM 専用)
#15 GB カートリッジ改造手順書 + フラッシュ書込み手順書
背景: NES 版は docs/cart-mod.md と docs/writer.md が実機化の知識を保持しており、特に writer.md の「実際の失敗確率順に並べたトラブルシュートラダー」が効いた。GBC 版でも同じ形をミラーする。ROM の完成を待たずに #2 の直後から独立に進められる。
スコープ In:
docs/cart-mod.md(GB カートリッジ改造手順)docs/writer.md(フラッシュ書込み手順 + トラブルシュートラダー)docs/hardware/*.md+ Pillow 生成の配線図docs/gb-package.md(シェル・ラベル・パッケージの寸法スペックシート)
スコープ Out: 実機での動作確認(ROM 完成後)
受入条件:
cart-mod.mdが NES 版の節構成をミラーしている: H1 に具体的な型番 → 1-4 行のリード(対象 issue / ドナー / チップ) →## 原理(根拠)(手順より先に根拠、各項目に一次資料の英語原文を引用) → 検証済み対応表 →## 作業手順(### 1.〜 のフェーズ見出し) → ジャンパ表 → 落とし穴 →## 参考writer.mdに## トラブルシュート: ID が読めない時の確認順があり、実際の発生確率順に 0. から番号づけられ、そう明言されている。症状 → 原因の対応が- 0xFF 0xFF → チップが応答していない(/CE か /OE の配線・電源を疑う)。の形で書かれている- 表が主データキャリアになっている(ピンアサイン / BOM / ジャンパ一覧 / バリアント比較)。警告はインラインの
⚠グリフ(VitePress の:::コンテナは使わない) - 数値がすべて実測かつ日付入り(
実測約 240 バイト/秒、2026-xx-xx の実機検証) - 図を含む文書の末尾に水平線 + 再生成コマンド 1 行がある
- 図ジェネレータが Pillow のみを使い、日本語フォントを
FONT_CANDIDATESからnext(p for p in ... if exists)で選ぶ(NES 版の共通規約) - 画像は
docs/hardware/img*.pngに置かれ、本文から相対リポジトリパスで参照される
タスク分解: ドナーカートリッジの調査 → ピン対応表 → 手順 → 配線図生成 → ラダーの執筆
テスト計画: 文書のリンク切れチェック。図ジェネレータの exit 0
#16 ビルド手順 / README / 説明書 LP / ドキュメントサイト
背景: NES 版は VitePress 2 サイト(説明書 LP + 開発ドキュメント)を Cloudflare Pages に出しており、docs/manual/parts/lp.md を単一ソースとして 2 サイトから include していた。GBC 版でも同じ構成を再現する。
スコープ In:
docs/build.md(RGBDS 導入、make ターゲット、ROM ヘッダの期待値を hex ダンプ + バイト注釈で、生成物の正確なバイト数)README.mdの更新docs/manual/parts/lp.md(説明書 LP 本文、単一ソース)- VitePress 設定 2 本と
srcExclude: ['**/parts/**'] - 長時間 E2E(S28)と全予算表の実測更新
スコープ Out: 新機能
受入条件:
docs/build.mdにmakeの生成物の正確なバイト数と、$0143/$0147/$0148を含むヘッダの hex ダンプ + バイト単位の注釈がある。タイトル領域は$0134-$013Eの 11 バイト($013F-$0142はメーカーコード、$0143は CGB フラグ)docs/manual/index.mdとmanual-site/index.mdがどちらも<!--@include: ...parts/lp.md-->のシムであり、LP を 1 回編集すれば両サイトが更新される- S28(完走): 曲 3(譜面のある曲で最長・最速)を
FLOW_PERFORMANCE開始からwFlowState == FLOW_CLEARに到達するまでtick()し、不変条件が一度も破れない。固定フレーム数でカウントしない(BGM 尺 4,150 フレームにカウントイン 96 フレームが加わる。曲 4 は 4,181 フレームで曲 3 より長いが譜面を持たない)。wFlowStateがFLOW_CLEARに変わる直前のフレームのhQHiwaterをラッチして 128 以下(e2e.md§4 のラッチ方式。FLOW_CLEAR到達後に読むとq_hiwater_reset済みで自明に通る) architecture.mdの予算表が実測値に置き換わっており、日付が入っている: §1 の D1、§4.4、§4.5、§4.7 の 4 箇所。⚠ §1 の D1 に載せた 4 値(2,394 / 4,660 / 4,114 / 6,180 dots)が §4.5 の同じ 4 値と一致することmake/make debug/make test/make e2eの 4 つがすべて exit 0docs/spec/aidopagaki-gb-design.md§12 がsrc/constants.incの全定数を含み、名前と値の両方が一致する(静的解析テスト)。BG マップアドレス 21 本とCHARTED_SONGS_MASKを含む- サイドバーが 4 グループ(ゲーム説明書 / ゲーム仕様書(正典)/ 設計書(技術)/ 開発・ハードウェア)で構成されている
タスク分解: build.md → README → LP → VitePress 設定 → 長時間 E2E → 予算表更新 → デプロイ
テスト計画: 全テストスイートの通し実行。VitePress のビルドが exit 0
4. 各 issue に共通で入れる受入条件
以下は 16 本すべての受入条件に自動的に含める(本書を参照する形で PR テンプレートに書く):
makeとmake debugが両方とも exit 0python3 -m unittest discover -s toolsが exit 0。⚠numpy/Pillowを入れていない環境でも exit 0 でなければならないので、それらを要するテスト(test_quantize_gbc.pyの減色再現性 3 件)はunittest.skipUnlessで括る(assets.md§6.1 / §8)python3 -m pytest tests -qが exit 0architecture.mdの予算表を実測値で更新していない状態で最適化を入れない- PR 本文に
hQHiwaterの実測値を貼る(NES 版「コミット本文が検証レポート」の継承)。hQHiwaterはシーン内の累積最大でシーン遷移のたびにq_hiwater_resetで 0 に戻るので、どのシーンの値かを明記し、wFlowStateが変わる直前のフレームの値をラッチして貼る(e2e.md§4 のラッチ方式) .symに載らないハードウェアレジスタ等の即値はtests/harness.pyの HW 定数表方式で集約され、同表の行以外に 16 進 4 桁または 10 進65xxxのアドレス直書きがない(check_conventions.shは HW 表のブロック行範囲だけを許可する)gbサブフォルダを 1 つも作らない: 下記のGB_SUBDIR_GREPがリポジトリ全体で 0 件
# GB_SUBDIR_GREP — 共通受入条件 7 の正典コマンド
grep -rEn '(tools|tests|build|src|docs|assets)/gb([^-.[:alnum:]]|$)' .⚠ 受入条件 2 / 3 / 7 は互いを縛っている。 本リポジトリは GB 専用なので、tools / tests / build / src の下に gb という名のサブフォルダは存在しない。そこにテストを置くと受入条件 2 のコマンド(-s tools)が「テストを 1 件も見つけずに」exit 0 を返すか、あるいは落ちる。
⚠ パターンの形に 2 つの罠がある。
- 末尾スラッシュに依存してはならない。
…/gb/だけを見るパターンは、末尾スラッシュなしでディレクトリを指す記述(unittest discoverの-s引数のような形)を素通りさせる。だからgbの直後を文字クラスで見る。 (/|\b)のように空になりうる選択肢を括弧に入れてはならない。\bは幅ゼロなので、ugrep(環境によってはgrepとして入っている実装)はempty (sub)expressionで exit 2 のエラー終了になる。exit 2 は「0 件だから合格」と区別がつかないので、壊れたパターンが「常に合格」として通り続ける。上記の文字クラス形は BSD grep / GNU grep / ugrep のいずれでも動く。\bは正当なファイル名を誤検出する。-や.は単語境界なので、\bを使うとdocs/gb-package.md(#15 の成果物)が毎回ヒットする。実際に BSD grep で確認済み。文字クラスから-と.を除いてあるのはこのためで、docs/gb-package.md/tools/gbgfx.py(#3 のライブラリ)/build/gbcはいずれもヒットしない。ヒットするのはgbの直後がスラッシュ・空白・引用符・行末になるディレクトリ指定だけである。
⚠ 本節は自分自身の検査を通る。 禁止パスを例示するために tools と gb を並べて書くと、この文書自身が GB_SUBDIR_GREP にヒットして受入条件 6 が永久に赤くなる。だから禁止対象は「tools / tests / build / src の下の gb ディレクトリ」と日本語で書き、リテラルのパスを本文に置かない。
⚠ CI では exit code も見る。 grep は 0 件で exit 1、ヒットで exit 0、エラーで exit 2 を返す。受入条件は「exit 1 であること」で判定し、exit 2 を合格に含めない。
5. NES 版の issue との対応(参考)
| GBC | NES での対応 issue |
|---|---|
| #2 | #1 環境構築 / #2 ビルドツールチェーン |
| #3 | #21 ダンススプライト振付 v4 |
| #4 | #23 ステージ背景+観客列 / #41 3 段観客スタンド |
| #5 | #17 リズムエンジン / #29 #33 ノーツレーン方式 / #62 空振りノーツ |
| #6 | #30 #32 Web トラッカー / #31 BGM ドライバ・DPCM |
| #7 | #35 曲別譜面 + BGM 拍同期 / #61 イージーモード |
| #8 | #26 #40 #49 講師スプライト |
| #9 | #27 #28 会話イベント・チュートリアル / #38 #39 観客ワイワイ化 |
| #10 | #48 #51 クリア画面 RANK + S 祝賀 / #66 ポーズ + 裏技 |
| #11 | #60 タイトル画面 |
| #12 | #60 プロローグ(NES は一枚絵なし) |
| #13 | (NES ではエンディングフォント生成のみ) |
| #14 | #44 全演出デバッグ ROM |
| #15 | #7 Mega フラッシュライター + 魔界島換装 / #34 実機載せ替え |
| #16 | #71 docs リフレッシュ |
6. 未決事項
| # | 論点 | 現状 | 判断時期 |
|---|---|---|---|
| I1 | 一枚絵 5 枚の元画選定とクロップ | assets/concept/*.png から起こす前提。人間の作業 | #11 の着手前 |
| I2 | 曲 4 の譜面新規作成 | 出荷後の追加 issue | 出荷後 |
| I3 | ROM 128 KB で 5 枚が収まらなかった場合 | 解消済み(#12 で実測)。bank6 ART_A 12,064 B / 16,384 B、bank7 ART_B 7,504 B / 16,384 B で 5 枚とも収まったので、$0148 = $03(256 KB)への引き上げは行わない | #12 |
| I4 | NOTE_SPEED_88 = 1.5 px/f の最終判断 | #5 のプレイテストで決める。退避先は 64f × 2px | #5 |
| I5 | CH1/CH2 のリトリガによる位相リセットが許容できない場合 | vol 系列の平坦化を強める。最悪はハードウェアエンベロープへ量子化 | #6 |
| I6 | SRAM セーブの追加 | 本版では入れない。追加時はヘッダ 2 バイト + save_commit 1 本 | 別 issue |