Skip to content

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 として残す
ビルド確認makemake 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#6APU ドライバ + ドラム再合成 + 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#10CLEAR / 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#15GB カートリッジ改造手順書 + フラッシュ書込み手順書#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.md
  • tools/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.pyexport_charts_gb.py は #6 / #7(サウンド・譜面)で実装する。本 issue では値そのものを文書に確定させ、後続 issue の受入条件が --check で再計算・突き合わせる形にする。受入条件 2 / 3 / 4 はその移管を反映して「文書側の値が機械検算に耐えること」を求める。

受入条件:

  1. 5 文書がすべて存在し、各文書の冒頭に「本書は何の正典か」の宣言が 1 行ある
  2. assets.md §1.1 の 15bit hex 24 件と全文書のインライン参照が R | (G<<5) | (B<<10) から再計算した値と全件一致し、全 BG パレットの c1 == $7FFF(#4 の gen_palettes.py --check が同じ検算をする)
  3. ノイズ表 16 エントリが f = 262144/(r × 2^shift)(code 0 → r = 0.5、上限 524,288 Hz)と全件一致(#6 の gen_noise_table.py --check が同じ検算をする)
  4. speed88 = 1911, 1792, 1977, 1509, 2389target[0] == 96 が仕様 §7.2 / §7.3 の式から再計算した値と一致(#7 の export_charts_gb.py --dry-run が同じ検算をする)
  5. 仕様 §12 の定数一覧が src/constants.inc と 1:1 対応する。BG マップアドレス 21 本と CHARTED_SONGS_MASK を含み、AUDIENCE_ANIM_FRAMES / NOTE_SPEED_88 の表記が全文書で揃っている
  6. 文書中のパスに gb サブフォルダが 1 箇所も現れない: §4 の GB_SUBDIR_GREPdocs/ に対して 0 件(exit 1)
  7. 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)と一致する
  8. 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.inc v5.3.0 の vendoring
  • Makefileall / debug / test / e2e / run / clean
  • Hello GBC ROM(フレームカウンタ + ボタン検出のみ)
  • 倍速モード切替rP1=$30rIE=0KEY1STOP
  • 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: ゲームロジック、アセット、サウンド

受入条件:

  1. make が exit 0 で build/aidopagaki.gbc を生成し、$0143 == $C0 / $0147 == $19 / $0148 == $02make debug も exit 0
  2. rgblink -n.sym を出力し、harness.sym["hFrameCount"] が解決できる。.sym に載らないハードウェアレジスタ等は tests/harness.pyHW 定数表に集約し、表の行以外では 16 進 4 桁または 10 進 65xxx のアドレスを直書きしないtools/check_conventions.sh の検査対象)。
  3. S0(決定性): 同一入力列を 2 回実行し、300 フレーム目のスクリーンショットがバイト一致hFrameCount の全履歴も一致
  4. wait_for_boot() が CGB ブート ROM(約 66-67 フレーム)を越えて 300 フレーム以内に成功する。Init クリアループ込みの実測は 83(2026-09-02、PyBoy 2.7.0)。壊れた ROM では RuntimeError を投げる
  5. hCurSpeed == 1 かつ KEY1 bit7 == 1(倍速に入っている)。SameBoy と BGB でも起動することを手動確認し PR 本文にスクリーンショットを貼る
  6. q_push を意図的に溢れさせると hQOverflow が増え、通常時は 0。hQHiwater が PyBoy から読め、flush ではリセットされず累積最大が残るq_hiwater_reset を呼んだときだけ 0 になることをテストで確認する)
  7. gdma_row が VBlank 内で完了し、呼出し時と転送完了時の両方で LY が 144-153 の範囲を出ない(フックの入口と出口で検査。e2e.md S4 ①)。gdma_bulk は入口の HL / DE / C を記録し、1 ≤ C ≤ 128、src/dst の 16 B 整列、dst + C*16 <= $A000、LCD OFF、入口/完了時の VBK が同一(0 または 1)であることを検査する
  8. LCD を OFF にする全書込みを命令サイトで捕捉し、wait_for_boot() 後の全フレームで、書込み値の rLCDC bit7 = 0 が LY >= 144 に限られる(フックで検査)。ON 側は検査しない(LCD OFF 中は VBlank が来ないため、ON を VBlank 中に行うことは原理的に不可能)
  9. HRAM / WRAM のシンボルが architecture.md §8 の接頭辞規約(HRAM = h、WRAM = w)に従っている
  10. 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.pygen_ai_sprite_gb.py
  • tools/test_chr_equivalence.py(ジェネレータ出力 == NES .chr のビット変換)
  • OBJ パレット OBP0/OBP1、8×16 メタスプライト描画、アイドル往復(半拍 16f)
  • 入力読み取り(正規化 + 起動時 1 回読み捨て)と 6 振りのポーズ状態機械(4 コマ × 5f = 20f)

スコープ Out: 講師、ノーツ、判定

受入条件:

  1. assets/ai-26f.2bpp3,328 Bai-gameover.2bpp256 B、再生成でバイト一致、SHA-256 ロックが一致
  2. test_chr_equivalence.py が green(ビットプレーン変換段階で NES .chr と完全一致)
  3. GB_TILE_ORDER == (0,2,4,6,1,3,5,7) が順列であること、REMAP の 4 行がすべて全単射であること、再マップ後の各タイルが非ゼロ index を 3 種類以内しか使わないことを unittest が検査
  4. OAM idx 0-3 の 4 OBJ でアイが表示され、tick() の per-line ≤ 10 が全フレームで成立
  5. 半拍(16 フレーム)ごとに frame 0/1 が往復する
  6. get_move_input が ← → ↑ ↓ A B の優先順で 1 つだけ返す。全 6 振りが 20 フレームで 4 コマ再生。ジャンプのみ pose_dy = 0,-2,-5,0、第 1 ステップで pose_dx が 2 倍
  7. リセット直後の 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 は使わない。LCDC bit3 は常に 0)
  • 観客 2 段 × 16 席(1 席 = 1 タイル幅 × 2 行)のランタイム合成
  • 視聴率ゲージ 16 タイル(gdma_row で更新)
  • HUD(SC: 6 桁 / AUD: 2 桁 / NRM

スコープ Out: キャラ、レーン、判定

受入条件:

  1. ../spec/aidopagaki-gb-design.md §2.1 の 18 行レイアウトが表示され、スクリーンショットが golden と一致
  2. BG タイル総数 ≤ 128 かつ max(tilemap) < 128 を unittest が検査(assert でビルドを落とす)
  3. 仕様 §12 の BG マップアドレス定数 21 本src/constants.inc にあり仕様 §2.3 と一致することを ASSERT が検査し、どの op も $xx00 をまたがないことを ASSERTharness.assert_no_page_cross() の両方が検査
  4. 全 BG パレットの c1 == $7FFF(unittest)。15bit 値が (R,G,B) からの再計算と全件一致。gen_palettes.py --check が exit 0
  5. BG マップが $9800 の 1 面だけであること: $9C00-$9FFF に書き込むコードが存在せず(grep)、rLCDC に書かれる値が $87 / $95 / $05 の 3 つだけである(フックで全書込みを記録して検査)。シーン切替は LCD OFF 窓で完結し、切替フレームのスクショが「途中の画面」にならない
  6. audience を 0→16 に注入すると点灯席数が n*2 で追随し、bg_row の期待列と一致。観客テーブルが ROM に存在しないことを .map ファイルの検査で確認
  7. 視聴率ゲージの点灯タイル数が audience と一致し、帯域境界(4/5、9/10、12/13)で BGP7 の c2 が変わる
  8. 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 画面

受入条件:

  1. ノーツ X が 16 + (rem*3)>>1 に一致(rem = 96, 64, 32, 8, 4, 0 の 6 点で検算)、遅刻は 16 にクランプ、rem > 96 は OAM Y = 0
  2. 最速曲相当の密度(23.58 f/拍)でレーン上ノーツが 6 枚を超えない。per-line OBJ が全フレーム ≤ 10(OAM X ≥ 168 のノーツも数えて
  3. 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
  4. S13(窓外の空振り): next_note_indextotal_* が不変(NES issue #62)
  5. S14: 窓外・誤ボタンでも pose が押した振りの先頭コマになる
  6. スコアが 999999 でクランプ。チュートリアル中は加算されない
  7. S16(観客増減 4 パターン) が全通過。0 で game_state == 1、16 でクランプ
  8. 判定文字の色が PERFECT=金 / GOOD=水色 / MISS=赤 に切り替わり、VRAM キューを 1 バイトも使わないhQHiwater が判定色変更で増えない)
  9. ld [wFlowState], aflow_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:

  • wApuShadow 23 B($C310$xx10 境界)+ wWaveShadow 16 B($C330$xx30 境界、同一ページ)、snd_write_reg 1 点経由の規約
  • ピッチの内部表現(div = 131072/f、書込み時のみ X = 2048 - div
  • 周期表 84 エントリ、ノイズ表 16 エントリ、vol_mul 256 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)

受入条件:

  1. grep -n 'ld \[\$FF[123][0-9A-F]\], a' src/snd_write_reg の中の 1 件だけ(APU 直書き禁止の機械検証)
  2. シャドウの境界整列: ASSERT LOW(wApuShadow) == $10 / ASSERT LOW(wWaveShadow) == $30 / ASSERT HIGH(wApuShadow) == HIGH(wWaveShadow) がビルド時に通る。snd_write_regld h, HIGH(wApuShadow) / ld l, c の 2 命令で両シャドウを振り分け、$FF30-$FF3F 用の分岐コードを持たないwApuShadow 23 B が PyBoy から読め、読み戻し可能なレジスタ($FF10 $FF11 $FF12 $FF16 … )についてはレジスタ別の読戻しマスクを適用したうえで実 APU の読み値と一致する。シャドウの書込先がチャンネル状態(wChanState = $C340-$C37F)と 1 バイトも重ならない
  3. div の値域検査: CH1/CH2 は 1 <= div <= 2047CH3 は 1 <= div <= 4095。MIDI 26(div = 3571)のベースが鳴ること。CH1/CH2 で MIDI ≤ 35、CH3 で MIDI ≤ 23 のノートに対しエクスポータが警告を出し 1 オクターブ上げる
  4. ノイズ表 16 エントリが f = 262144/(r × 2^shift)(code 0 → r=0.5、上限 524,288 Hz)で再計算した値と一致。gen_noise_table.py --check が exit 0
  5. ドラム表が 128 B(kick 12f / snare 9f / tom 11f)で、architecture.md §7.7 の 4 B ビット割当(B0/B1 = CH3 X 11bit + NR32 2bit + 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 フレーム余分に途切れる)
  6. ref_sim_gb.py と 1,500 フレーム分の書込みイベント列 (frame, reg_lo, value) が完全一致(最低 1 曲、DRUM_USE_WAVE の両モード)。イベントの本数も一致すること(同値 NRx4 の余計なリトリガを検出するため。シャドウの値スナップショット比較では検出できない)
  7. wave RAM は sound_init の 1 回だけ書かれる(書込み回数カウンタで検査)
  8. fx 0xy 1xx 2xx 3xx 4xy Vxx が NES 版と同じ符号方向で動く(ref sim が検出)
  9. 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.pyGB_FPSNOTE_APPROACH_FRAMES = 96
  • 曲と譜面を同一バンク(bank1)に置く不変条件
  • カウントイン 96 フレーム → sound_play_song
  • コーチの独立譜面カーソル(coach_chart_pos
  • 曲進行(RANK A 以上でクリア扱い、B 以下でリトライ)
  • EASY モード(偶数拍の休符化、$FF 終端は除外)
  • 本番中 SELECT の曲やり直し(restart_pending

スコープ Out: CLEAR 画面(#10)

受入条件:

  1. speed881911 / 1792 / 1977 / 1509 / 2389target[0] == 96、単調増加、$FF 終端、CHARTED_SONGS_MASK == %00001111
  2. S6: snd_playing が 0→1 になるフレームで song_frame == 96 ちょうど(±0)
  3. 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 曲で必ず落ちる)
  4. 5 曲すべてで ref_sim_gb.py と 1,500 フレームのイベント列一致。5 曲の実時間が仕様 §7.2 の表と ±0.05 s 以内
  5. 曲と譜面が同一バンクにあることを .map ファイルの検査で確認
  6. S20(EASY): easy_mode == 1 で偶数 index の拍がすべて REST。$FF 終端は残り、譜面が正常終了する
  7. S9(曲進行): RANK A 以上で cleared_mask のビットが立ち次曲へ、B 以下で同一曲リトライ。曲 4 は選曲・進行から除外される
  8. 本番中 SELECT で次の曲に切り替わり、再カウントインが 96 フレームで走る。hQHiwater が 128 を超えない

タスク分解: export_charts_gb.py → バンク配置 → カウントイン → コーチカーソル → 進行 → EASY → SELECT

テスト計画: test_export_charts_gb.pytests/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)

受入条件:

  1. assets/coach-32f.2bpp4,096 B、32 フレーム、bank1 にちょうど収まる(256/256 タイル)
  2. OBJ 属性 bit3 = 1 で bank1 のタイルが引かれ、アイ(bank0)と同時に正しく表示される
  3. 自己書換えストリーミングのコードが 1 行も存在しないgrep -c 'coach_stream' src/ が 0)
  4. アイと同一 Y 帯で per-line OBJ ≤ 10(実測 4 枚)
  5. 入場演出で歩行 4 コマ(26-29)が再生され、COACH_X が 0 から 48 まで移動して停止する
  6. 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 .chr 3 本 → GB 2bpp)
  • かな窓の GDMA 入替(会話 54 / プロローグ 60 / エンディング 76)
  • BG 属性 bit3 による英数・かなの行内混在
  • reflow_text.py と逐語 unittest
  • テキストエンジン(8 フレームに 1 行、1 フレーム 1 行だけキューに積む)
  • 会話 3 ページ、チュートリアル 6 ノーツの完成、Lv4 ハイプ演出

スコープ Out: プロローグ(#12)、エンディング(#13)

受入条件:

  1. かな 3 種のタイル数が 54 / 60 / 76、すべての使用文字にグリフがある(未定義文字の化けを防ぐ。NES 申し送りの罠)
  2. 全 50 行(プロローグ 12 + 会話 8 + エンディング 30)が 20 タイル以内(最長 18)、1 ページ 4 行以内。TIKTOK が ROM 内に存在せず SNS が存在する。⚠ 行数と最長タイル数は test_text_pages.py が毎回再計算し、その出力を正とする
  3. 逐語一致テストが green(NES 原文をテストに書き込み、TIKTOK → SNS の 1 箇所以外は完全一致。コピー承認ゲート)
  4. かな窓の入替が LCD OFF 窓で完結し、hQHiwater が 128 を超えない。flow_enter_ending がエンディングかなの入替を呼ぶことを静的解析テストが確認
  5. S26: 全ページで bg_row のタイル列が reflow_text.py の期待列と完全一致。8 フレームに 1 行ずつ現れる
  6. S25: チュートリアル譜面が 4 拍に 1 ノーツ × 6 グループで L→R→U→D→A→B。SELECT で最終セリフへスキップ。メトロノーム(880 Hz / 8f)が apu_shadow に出る
  7. S18(Lv4 ハイプ): audience >= 13 で A/B を PERFECT → BGP1/BGP3 が 2 フレームだけ $7FFF、CH4 に 12 フレームの歓声。パレットシャドウから正しく復帰する
  8. S5(フル導線): flow_state が 9 → 8 → 0 → 1 → 4 → 2 → 3 → 5 → 6 の順に遷移

タスク分解: かな変換 → 窓入替 → 属性混在 → reflow_text.py → 逐語テスト → テキストエンジン → 会話 → チュートリアル → Lv4

テスト計画: test_kana_font_gb.py / test_text_pages.pytests/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)

受入条件:

  1. S19(ランク閾値 5 ケース): (6,0,10)→D (7,0,9)→C (10,0,6)→B (15,1,0)→A (16,0,0)→S
  2. ランク算出に除算ヘルパが使われていない(grep で確認)
  3. バナー文字列がすべて 20 タイル以内(GAME OVER START 16 / RETRY - START 13 / NEXT SONG - START 17 / ALL CLEAR - START 17)
  4. S23: 紙吹雪 8 枚が OAM idx 8-15 に存在し Y が単調増加。confetti_lfsrx^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
  5. S24: ゲームオーバーでアイ pose 26/27、講師 30/31 が交互になる
  6. S21(ポーズ): START で pause_flag == 1snd_row が凍結、バナーが PAUSE。再 START で snd_row が進む。ポーズ解除フレームの hQHiwater が 128 以下消音が NR51 = 0 だけで行われ、snd_silenceNR52 = 0)がポーズ経路から呼ばれないgrep と書込みイベント列の両方で確認)
  7. S22(裏技 3 種) がすべて動作する
  8. 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)

受入条件:

  1. assets/art/title.png が 160×96 のインデックス PNG、PLTE ≤ 32 色、1 タイルあたり ≤ 4 色絵が使うパレットが 0-6 の 7 本以内(BGP7 はテキスト帯用の固定色)
  2. 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 検査と .tilemap 240 B 検査、および test_quantize_gbc.py の出力 160×96 検査で受け持つ(assets.md §5.3 / §5.4)
  3. quantize_gbc.py が Makefile の依存に現れないgrep で確認)。ビルドは PNG と .gbcpal からのみ走る
  4. S27: ART シーンで LCDC == $95。BG マップ row 0-11 が絵、row 13-15 がメニュー、row 17 が MODE。row 12-17 のタイルの BG 属性 bit3 が 1(テキストが bank1 から引かれている)
  5. メニュー 4 行のユニークグリフ 16 種(H を含まない)+ カーソル がすべて bank1 の UI 英数フォント 47 から引かれている(bank0 に英数タイルを置かない)。bg_row(13,0,20)bg_row(17,0,20) のタイル列と属性列が期待列と完全一致
  6. ART → GAME 復帰後にアイと講師の OBJ CHR が正しく再ロードされている(復帰直後のスクショが 03_ai_poses.png の該当部分と一致)
  7. 分割 GDMA が**1 転送 1-128 ブロック、src/dst とも 16 B 整列、dst + blocks*16 <= $A000**で、VRAM バンクを跨がない(入口の CPU レジスタと転送完了時の VBK をフックで記録して検査)
  8. START / CONTINUE / PROLOGUE の 3 項目が動く。← → SELECT で NORMAL ⇄ EASY
  9. S8(アトラクト): 600 フレーム無操作で wFlowState == 10
  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.pytest_quantize_gbc.pytests/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: エンディング

受入条件:

  1. 4 枚すべてが -N 240 を通り、各 .2bpp が ≤ 3,840 B、.tilemap / .attrmap が各 240 B、PLTE ≤ 32 色、絵が使うパレットが 0-6 の 7 本以内
  2. 4 ページ × 3 行が 20 タイル以内で表示され、bg_row のタイル列が期待列と完全一致
  3. テキストは 8 フレームに 1 行で現れる(1 ページ = 24 フレームで表示完了)。タイプライタ演出ではない
  4. A で次ページへ進む。アトラクト時は 240 フレームで自動送りされ、1 ページを送り切る前に次ページへ行かない(3 行 × 8f = 24f << 240f)
  5. TIKTOK が ROM 内に存在しない。プロローグ 1-2 が いつものように SNSを である(逐語テスト)
  6. スクショ 4 枚が golden 一致
  7. bank6 / bank7 に 5 枚すべてが収まる(1 枚 4,384 B → bank6 13,152 B / bank7 8,768 B + cold code。.map の検査。超えたら $0148 = $03 へ上げる判断を PR 本文に書く)
  8. ページ遷移中に tick() の不変条件が破れない

受入条件 9(人間承認): 4 枚の -preview.png をレビュー担当(Fable)がプレビューを目視承認する。

タスク分解: 元画のクロップ → 減色 4 枚 → 人間レビュー → ページ差し替え → フェード → 送り → アトラクト

テスト計画: test_art_assets.py(5 枚に拡張)、test_quantize_gbc.pytests/test_art.py


#13 エンディング 12 ページ + 曲 4

背景: 全曲クリア後のご褒美。エンディングかな 76 タイルは会話 54 と合わせると 130 > 128 でかな窓に入らないため、シーン入場での入替が必須になる。曲 4(トワイライト・スターズ)は譜面を持たずエンディング専用。

スコープ In:

  • エンディングかな 76 タイルの窓入替
  • 12 ページのテキスト進行(ビジョン内、一枚絵ではない)
  • 曲 4 の同時再生
  • 12 ページ後のタイトル復帰

スコープ Out: 曲 4 の譜面作成(出荷後)

受入条件:

  1. 12 ページの本文が逐語一致(unittest がコピー承認ゲート)。全行 20 タイル以内
  2. エンディングかな 76 タイルが窓に収まり、flow_enter_ending が入替を呼ぶことを静的解析テストが確認
  3. 曲 4 が同時再生され ref_sim_gb.py と 1,500 フレーム一致
  4. S10: cleared_mask == %00001111 で CONTINUE が SONG SELECT に変わる。CLEAR で START → flow_state == 12 → 12 ページ → flow_state == 9
  5. 12 ページ通しで tick() の不変条件が破れない
  6. 50_ending_p12.pngTHE END)が golden 一致

タスク分解: かな窓入替 → ページ進行 → 曲 4 再生 → タイトル復帰

テスト計画: test_text_pages.pytests/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 への影響

受入条件:

  1. make debug が exit 0 で 12 項目メニューを表示する
  2. 各項目が対応する演出へ遷移し、B / START でメニューへ戻る(12 項目すべてを E2E で巡回)
  3. 10 VRAM QUEUE STRESShQHiwater の実測値を画面に表示し、その値を PR 本文に貼って予算表を更新する
  4. 12 SPEED TOGGLE で通常速に落とし、VBlank に流す GDMA を gdma_row × 2(64 B)までに抑えた区間を完走して hQOverflow == 0architecture.md §4.5 の通常フレーム 102.2% を前提に、通常速フォールバックではキュー/GDMA量を追加制限・分割して完走させる)。⚠ 通常速で VBlank GDMA 上限 1,024 B を撃つと VBlank を 135.5% 超過して破綻する。 このモードでは GDMA 上限を 32 ブロック/フレームに落として 2 フレームに分割すること。通常速の 102.2% は通常フレーム(GDMA なし)を含む現在の予算値であり、全フレームを無制限に流せるという意味ではない
  5. 11 PALETTE VIEWER が BG 8 本 / OBJ 8 本を表示する
  6. 製品 ROM(make)に DEBUG コードが 1 バイトも入らない.map のサイズ比較で確認)

タスク分解: メニュー UI → 12 項目 → リングログ → 速度トグル → パレットビューア

テスト計画: tests/test_debug_menu.py(debug ROM 専用)


#15 GB カートリッジ改造手順書 + フラッシュ書込み手順書

背景: NES 版は docs/cart-mod.mddocs/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 完成後)

受入条件:

  1. cart-mod.md が NES 版の節構成をミラーしている: H1 に具体的な型番 → 1-4 行のリード(対象 issue / ドナー / チップ) → ## 原理(根拠)手順より先に根拠、各項目に一次資料の英語原文を引用) → 検証済み対応表 → ## 作業手順### 1. 〜 のフェーズ見出し) → ジャンパ表 → 落とし穴 → ## 参考
  2. writer.md## トラブルシュート: ID が読めない時の確認順 があり、実際の発生確率順に 0. から番号づけられ、そう明言されている。症状 → 原因の対応が - 0xFF 0xFF → チップが応答していない(/CE か /OE の配線・電源を疑う)。 の形で書かれている
  3. 表が主データキャリアになっている(ピンアサイン / BOM / ジャンパ一覧 / バリアント比較)。警告はインラインの グリフ(VitePress の ::: コンテナは使わない)
  4. 数値がすべて実測かつ日付入り(実測約 240 バイト/秒2026-xx-xx の実機検証
  5. 図を含む文書の末尾に水平線 + 再生成コマンド 1 行がある
  6. 図ジェネレータが Pillow のみを使い、日本語フォントを FONT_CANDIDATES から next(p for p in ... if exists) で選ぶ(NES 版の共通規約)
  7. 画像は 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: 新機能

受入条件:

  1. docs/build.mdmake の生成物の正確なバイト数と、$0143 / $0147 / $0148 を含むヘッダの hex ダンプ + バイト単位の注釈がある。タイトル領域は $0134-$013E の 11 バイト$013F-$0142 はメーカーコード、$0143 は CGB フラグ)
  2. docs/manual/index.mdmanual-site/index.md がどちらも <!--@include: ...parts/lp.md--> のシムであり、LP を 1 回編集すれば両サイトが更新される
  3. S28(完走): 曲 3(譜面のある曲で最長・最速)FLOW_PERFORMANCE 開始から wFlowState == FLOW_CLEAR に到達するまで tick() し、不変条件が一度も破れない。固定フレーム数でカウントしない(BGM 尺 4,150 フレームにカウントイン 96 フレームが加わる。曲 4 は 4,181 フレームで曲 3 より長いが譜面を持たない)。wFlowStateFLOW_CLEAR に変わる直前のフレームの hQHiwater をラッチして 128 以下e2e.md §4 のラッチ方式。FLOW_CLEAR 到達後に読むと q_hiwater_reset 済みで自明に通る)
  4. 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 値と一致すること
  5. make / make debug / make test / make e2e の 4 つがすべて exit 0
  6. docs/spec/aidopagaki-gb-design.md §12 が src/constants.inc の全定数を含み、名前と値の両方が一致する(静的解析テスト)。BG マップアドレス 21 本と CHARTED_SONGS_MASK を含む
  7. サイドバーが 4 グループ(ゲーム説明書 / ゲーム仕様書(正典)/ 設計書(技術)/ 開発・ハードウェア)で構成されている

タスク分解: build.md → README → LP → VitePress 設定 → 長時間 E2E → 予算表更新 → デプロイ

テスト計画: 全テストスイートの通し実行。VitePress のビルドが exit 0


4. 各 issue に共通で入れる受入条件

以下は 16 本すべての受入条件に自動的に含める(本書を参照する形で PR テンプレートに書く):

  1. makemake debug両方とも exit 0
  2. python3 -m unittest discover -s tools が exit 0。⚠ numpy / Pillow を入れていない環境でも exit 0 でなければならないので、それらを要するテスト(test_quantize_gbc.py の減色再現性 3 件)は unittest.skipUnless で括る(assets.md §6.1 / §8)
  3. python3 -m pytest tests -q が exit 0
  4. architecture.md の予算表を実測値で更新していない状態で最適化を入れない
  5. PR 本文に hQHiwater の実測値を貼る(NES 版「コミット本文が検証レポート」の継承)。hQHiwater はシーン内の累積最大でシーン遷移のたびに q_hiwater_reset で 0 に戻るので、どのシーンの値かを明記し、wFlowState が変わる直前のフレームの値をラッチして貼るe2e.md §4 のラッチ方式)
  6. .sym に載らないハードウェアレジスタ等の即値は tests/harness.pyHW 定数表方式で集約され、同表の行以外に 16 進 4 桁または 10 進 65xxx のアドレス直書きがない(check_conventions.sh は HW 表のブロック行範囲だけを許可する)
  7. gb サブフォルダを 1 つも作らない: 下記の GB_SUBDIR_GREP がリポジトリ全体で 0 件
bash
# 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 つの罠がある。

  1. 末尾スラッシュに依存してはならない。 …/gb/ だけを見るパターンは、末尾スラッシュなしでディレクトリを指す記述(unittest discover-s 引数のような形)を素通りさせる。だから gb の直後を文字クラスで見る。
  2. (/|\b) のように空になりうる選択肢を括弧に入れてはならない。 \b は幅ゼロなので、ugrep(環境によっては grep として入っている実装)は empty (sub)expressionexit 2 のエラー終了になる。exit 2 は「0 件だから合格」と区別がつかないので、壊れたパターンが「常に合格」として通り続ける。上記の文字クラス形は BSD grep / GNU grep / ugrep のいずれでも動く。
  3. \b は正当なファイル名を誤検出する。 -. は単語境界なので、\b を使うと docs/gb-package.md(#15 の成果物)が毎回ヒットする。実際に BSD grep で確認済み。文字クラスから -. を除いてあるのはこのためで、docs/gb-package.md / tools/gbgfx.py(#3 のライブラリ)/ build/gbc はいずれもヒットしない。ヒットするのは gb の直後がスラッシュ・空白・引用符・行末になるディレクトリ指定だけである。

本節は自分自身の検査を通る。 禁止パスを例示するために toolsgb を並べて書くと、この文書自身が 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 との対応(参考)

GBCNES での対応 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出荷後
I3ROM 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
I4NOTE_SPEED_88 = 1.5 px/f の最終判断#5 のプレイテストで決める。退避先は 64f × 2px#5
I5CH1/CH2 のリトリガによる位相リセットが許容できない場合vol 系列の平坦化を強める。最悪はハードウェアエンベロープへ量子化#6
I6SRAM セーブの追加本版では入れない。追加時はヘッダ 2 バイト + save_commit 1 本別 issue