AIドパガキ GBC 版 アセット規約
本書は タイル配置・パレット値・アセット生成パイプラインの正典である。タイル index、15bit RGB 値、ジェネレータの入出力が他文書と食い違った場合、本書を正とする。
参照: ../spec/aidopagaki-gb-design.md(画面レイアウト)/ architecture.md(VRAM 構成・ROM バンク)
0. 制作方式(NES 版から継承する確定ルール)
NES 版 docs/design/assets.md より逐語:
自動画像変換は禁止 画像 → CHR の自動変換は減色ノイズで一度失敗済み。 **「帯パレット先行設計 → ピクセルマップ手打ち → Python ジェネレータで CHR 化」**が正のワークフロー。
GBC 版での扱い:
| 対象 | 方式 |
|---|---|
| キャラ(アイ 26F / 講師 32F)・ステージ BG・ノーツ・フォント | ルールを厳守。 ピクセルマップ(または NES 版 .chr)からジェネレータで 2bpp 化する。画像入力なし |
| 一枚絵 5 枚 | 例外。ただしビルドから減色を追い出す。 減色は tools/quantize_gbc.py でオフラインに 1 回だけ行い、出力の 32 色インデックス PNG を正典としてコミットする。以後 rgbgfx -c gbc:<name>.gbcpal(量子化ツールが出力する RGB555 LE 64 B の明示パレット)を読むだけで、ビルドは決定的になる |
⚠ 一枚絵の減色は必ず 4 倍プレビュー PNG による人間レビューを通す。 減色ノイズで一度失敗した工程を復活させる以上、「ビルドが通る」ことは品質の保証にならない。承認された PNG だけがコミットされる。
1. パレット
15bit BGR555。色 = R | (G << 5) | (B << 10)(各 0-31)。BGPD / OCPD への書込みはリトルエンディアン 2 バイト(下位バイト先)。
⚠ 15bit 値は手打ちしない。 tools/gen_palettes.py が (R,G,B) から機械生成し、src/palette_data.inc と本書の表の両方を出力する。下表の hex は生成器の出力を貼ったものであり、(R,G,B) との一致は unittest が検算する。
⚠ 「近似 #RRGGBB」列は floor(v * 255 / 31) による表示用の目安であり、正典ではない。ROM に入るのは RGB555 の 15bit 値だけである。実機・エミュレータが 5bit を 8bit へ引き伸ばす方式は実装ごとに違う(ビット複製 (v<<3)|(v>>2) を使う実装が多い)ので、この列の値でスクリーンショットを比較してはならない。ゴールデン比較は PyBoy の出力どうしで行う(e2e.md §7)。
1.1 共通色定義
| 名前 | R,G,B | RGB555 | 近似 #RRGGBB |
|---|---|---|---|
C_BLACK | 0,0,0 | $0000 | #000000 |
C_WHITE | 31,31,31 | $7FFF | #FFFFFF |
C_CYAN | 8,28,31 | $7F88 | #41E6FF |
C_LTCYAN | 16,30,31 | $7FD0 | #83F6FF |
C_NAVY | 2,4,12 | $3082 | #102062 |
C_DKNAVY | 1,2,6 | $1841 | #081031 |
C_MAGENTA | 31,6,24 | $60DF | #FF31C5 |
C_PINK | 31,18,22 | $5A5F | #FF94B4 |
C_HOTPINK | 30,12,20 | $519E | #F662A4 |
C_SKIN | 31,24,19 | $4F1F | #FFC59C |
C_HAIR | 18,10,6 | $1952 | #945231 |
C_PURPLE | 16,6,26 | $68D0 | #8331D5 |
C_ORANGE | 31,20,4 | $129F | #FFA420 |
C_AMBER | 31,16,0 | $021F | #FF8300 |
C_YELLOW | 31,30,8 | $23DF | #FFF641 |
C_GOLD | 31,26,4 | $135F | #FFD520 |
C_GREEN | 10,26,12 | $334A | #52D562 |
C_LIME | 18,31,10 | $2BF2 | #94FF52 |
C_RED | 28,4,6 | $189C | #E62031 |
C_DKRED | 12,0,2 | $080C | #620010 |
C_BLUE | 6,12,30 | $7986 | #3162F6 |
C_GRAY | 14,14,16 | $41CE | #737383 |
C_DKGRAY | 6,6,8 | $20C6 | #313141 |
C_OUTLINE | 2,1,4 | $1022 | #100820 |
1.2 BG パレット 8 本(GAME 構成)
⚠ 申し送りルールの継承: 全 BG パレットの c1 = C_WHITE($7FFF)を共有する。 NES 版「全パレットの color1 = $30(白)共有がテキスト可読性の要」をそのまま守る。判定色の 3 パターン(§1.3)でも破らない。
⚠ これは「UI 英数フォント(gen_ui_font_gb.py、bank0 $00-$2E)の描画規則」である。 UI 英数の 47 グリフはパレット index 1 だけで描き、どのパレットに載せても白で読める。判定グリフはこの規則の唯一の例外で、パレット index 2 で描く(§2.2)。判定文字は「読めること」ではなく「PERFECT / GOOD / MISS を色で区別すること」が要件であり、c1 を固定したまま c2 を差し替える §1.3 の仕組みは、文字が index 2 を参照して初めて画面に出るためである。⚠ c1 の白固定を判定パレットで破って解決してはならない。
| # | 用途 | c0 | c1 | c2 | c3 |
|---|---|---|---|---|---|
| BGP0 | HUD 行 0(SC: / AUD: / 数字) | C_DKNAVY $1841 | C_WHITE $7FFF | C_CYAN $7F88 | C_NAVY $3082 |
| BGP1 | ビジョン内部・枠・バナー・レーンレール・かなテキスト | C_BLACK $0000 | C_WHITE $7FFF | C_CYAN $7F88 | C_NAVY $3082 |
| BGP2 | 判定文字(実行時に書換、§1.3) | C_BLACK $0000 | C_WHITE $7FFF | C_GOLD $135F | C_AMBER $021F |
| BGP3 | 電脳グリッド壁 | C_BLACK $0000 | C_WHITE $7FFF | C_PURPLE $68D0 | C_DKGRAY $20C6 |
| BGP4 | ステージ床・ネオンエッジ | C_DKGRAY $20C6 | C_WHITE $7FFF | C_CYAN $7F88 | C_GRAY $41CE |
| BGP5 | 観客 前段(暖色) | C_NAVY $3082 | C_WHITE $7FFF | C_ORANGE $129F | C_MAGENTA $60DF |
| BGP6 | 観客 後段(寒色) | C_NAVY $3082 | C_WHITE $7FFF | C_LTCYAN $7FD0 | C_GREEN $334A |
| BGP7 | 視聴率ゲージ(c2 を帯域で書換、§1.4) + MODE 表示 | C_DKNAVY $1841 | C_WHITE $7FFF | C_DKRED $080C | C_DKGRAY $20C6 |
⚠ BG 属性はタイル単位なので、観客席のタイルごとに BGP5 / BGP6 を交互に割り当てて色をばらつかせられる(NES の 16×16 px 単位では不可能だった)。属性マップは静的で、実行時に触るのはタイル索引だけ。
1.3 判定色(BGP2 の 8 バイト書換え)
| 判定 | c0 | c1 | c2 | c3 |
|---|---|---|---|---|
| PERFECT(金) | $0000 | $7FFF | C_GOLD $135F | C_AMBER $021F |
| GOOD(水色) | $0000 | $7FFF | C_LTCYAN $7FD0 | C_CYAN $7F88 |
| MISS(赤) | $0000 | $7FFF | C_RED $189C | C_DKRED $080C |
⚠ 判定文字のタイルは差し替えない。 色だけが変わるので VRAM キューを 1 バイトも消費しない(BGPI 1 + BGPD 8 の書込みのみ、約 40 M-cycle)。⚠ c2 が画面に出るのは判定グリフが index 2 で描いてあるからである(§1.2 / §2.2)。UI 英数フォントで判定行を組むと、c2/c3 をいくら差し替えても文字は白のままになる。
1.4 視聴率ゲージの帯域色(BGP7 の c2 を書換)
| 観客 n | c2 |
|---|---|
| 1-4 | C_DKRED $080C |
| 5-9 | C_AMBER $021F |
| 10-12 | C_GOLD $135F |
| 13-16(Lv4 帯) | C_WHITE $7FFF(8 フレーム周期で c3 と交互 = 点滅) |
1.5 Lv4 ハイプ フラッシュ
観客 13 以上で A/B を PERFECT した瞬間、BGP1 と BGP3 の 4 色を 2 フレームだけ $7FFF に置換する(16 B、約 60 M-cycle)。パレットシャドウ(architecture.md §8.1)から復帰する。NES 版「バックドロップを白 $30 に 2 フレーム」の等価。
1.6 OBJ パレット 8 本(c0 = 透明)
⚠ NES の 3 帯(タイル行 0-1 / 行 2 / 行 3)は 8×16 OBJ では成立しない。 8×16 OBJ は 1 枚につきパレットを 1 本しか持てず、y16-31 の 16 px を 1 本で塗る。したがって上 16 px / 下 16 px の 2 帯に再編し、失われる 1 帯分の色は §4 のインデックス再マップで吸収する。
| # | 用途 | c1 | c2 | c3 |
|---|---|---|---|---|
| OBP0 | アイ 上 16px(頭・上半身) | C_OUTLINE $1022 | C_SKIN $4F1F | C_CYAN $7F88 |
| OBP1 | アイ 下 16px(腰・脚・靴) | C_NAVY $3082 | C_PINK $5A5F | C_WHITE $7FFF |
| OBP2 | 講師 上 16px | C_OUTLINE $1022 | C_SKIN $4F1F | C_PURPLE $68D0 |
| OBP3 | 講師 下 16px | C_PURPLE $68D0 | C_MAGENTA $60DF | C_WHITE $7FFF |
| OBP4 | 判定マーカー ◎ | C_WHITE $7FFF | C_CYAN $7F88 | C_NAVY $3082 |
| OBP5 | ノーツ 矢印 ← → ↑ ↓ | C_WHITE $7FFF | C_CYAN $7F88 | C_NAVY $3082 |
| OBP6 | ノーツ A(赤)/ B(青) | C_WHITE $7FFF | C_RED $189C | C_BLUE $7986 |
| OBP7 | 紙吹雪 | C_RED $189C | C_YELLOW $23DF | C_CYAN $7F88 |
NES の元パレットとの対応(参考):
| NES 帯 | NES 値 | 色 | GBC での扱い |
|---|---|---|---|
| sprite pal0(タイル行 0-1) | $0F,$00,$36,$2C | 黒 / ピンク / 水色 | OBP0(黒 → 輪郭、肌、髪へ再定義) |
| sprite pal1(タイル行 2) | $0F,$00,$30,$36 | 黒 / 白 / ピンク | OBP1 に統合(index 再マップ) |
| sprite pal2(タイル行 3) | $0F,$36,$30,$2C | ピンク / 白 / 水色 | OBP1 に統合(index 再マップ) |
| sprite pal3 | $0F,$16,$12,$30 | 黒 / 赤 / 青 / 白 | OBP6 |
⚠ NES では紙吹雪を 4 パレットで撒いていた。GBC では OBP6 / OBP7 の 2 本 × タイル 2 形状 × X/Y フリップ = 8 通りのバリエーションで等価な見た目を得る。
1.7 ART 構成のパレット
一枚絵ごとに quantize_gbc.py が生成した 8 本 × 4 色をそのまま BGPD に流し込む(64 B)。ただし:
- BGP7 は下部テキスト帯用に固定予約(
C_BLACK$0000/C_WHITE$7FFF/C_NAVY$3082/C_DKGRAY$20C6)。絵は BGP0-BGP6 の 7 本だけで量子化する(quantize_gbc.pyのN_IMAGE_PALETTES = 7。出力する.gbcpalは BGP7 を含むN_PALETTES = 8本 64 B) - テキストの可読性のため BGP7 の c1 も
$7FFF(§1.2 のルールを ART でも守る) .gbcpalは 8 本 × 4 色 × 2 B = 64 B を出力する。末尾の 8 本目が固定の BGP7 であり、rgbgfx -n 8 -s 4はそれを含めて 8 本として扱うtest_art_assets.pyが「絵のタイルが参照するパレットが 0-6 の 7 本以内」を検査する。PLTE 32 色・1 タイル 4 色以内の検査だけでは、絵が BGP7 を使ってしまった事故を検出できない
2. タイル配置
2.1 BG タイル(GAME 構成、VRAM bank0 $9000-$97FF、128 タイル)
| index | 内容 | 数 |
|---|---|---|
$00 | 空白 | 1 |
$01-$1A | A-Z | 26 |
$1B-$24 | 0-9 | 10 |
$25-$2C | : - ! ? . , ( ) | 8 |
$2D-$2E | ▼(送りプロンプト)▶(カーソル) | 2 |
| 小計: UI 英数フォント | 47 | |
$2F-$3E | ビジョン枠・角・水平線 | 16 |
$3F-$4A | 電脳グリッド壁 | 12 |
$4B-$52 | ステージ床エッジ(ネオン)・床 | 8 |
$53-$5A | レーン レール(マーカー位置マーキング含む) | 8 |
$5B-$62 | 観客 4 アニメ × 上下 2 半身 | 8 |
$63-$64 | 視聴率ゲージ(消灯 / 点灯) | 2 |
$65-$68 | 段差・手すり・スピーカー・照明 | 4 |
$69-$6F | ステージ小物 予備 | 7 |
$70-$7F | 予備 | 16 |
| 合計 | 128 |
⚠ BG マップ値は 0-127 に制限される(architecture.md §3.1)。gen_stage_bg_gb.py に assert max(tilemap) < 128 を入れ、ビルド時に落とす。
2.2 BG タイル(GAME 構成、VRAM bank1 $9000-$97FF、128 タイル)
bank1 の BG 窓は かなフォント窓(入替)と判定グリフ(常駐)の 2 区画に分ける。
| index | 内容 | タイル | 常駐 |
|---|---|---|---|
$00-$72 | かなフォント窓(シーン入場時に入替) | ≤ 115 | 入替 |
$73 | 判定行の空白(パレット index 0) | 1 | 常駐 |
$74-$7F | 判定グリフ P E R F C T G O D M I S(パレット index 2) | 12 | 常駐 |
| 合計 | 128 |
かなフォント窓の中身:
| シーン | 内容 | タイル |
|---|---|---|
| 会話 1-3 | 会話かな | 54 |
| プロローグ(ART では bank1 の別領域) | プロローグかな | 60 |
| エンディング | エンディングかな | 76 |
⚠ エンディング 76 + 会話 54 = 130 > 115。 同時には出ないのでシーン入場時に入れ替える。test_kana_font_gb.py がタイル数を検査し、flow_enter_ending が入替を呼ぶことを静的解析テストで保証する。⚠ 窓の上限は 128 ではなく KANA_TILE_LIMIT = 115 である(末尾 13 タイルが判定グリフの常駐領域)。この値は tools/gen_judge_font_gb.py が src/judge_font_data.inc に出力する。
BG 属性 bit3 = 1 でかな側のバンクを選ぶため、1 行の中に英数とかなを混在できる(例: アイ「なかみは いくらかなー?、みんなも SNSを みるだけでなく)。
判定グリフ(tools/gen_judge_font_gb.py):
⚠ §1.2 の「全 BG パレット c1 = 白」は UI 英数フォントの規則であり、判定グリフはその唯一の例外として index 2 で描く。 UI 英数の 47 グリフ(bank0 $00-$2E)は index 1 だけで描かれるので、BGP2 の c2/c3 を差し替えても判定文字は白のまま画面に出ない。判定文字の要件は「読めること」ではなく「PERFECT / GOOD / MISS を色で区別すること」なので、専用グリフを index 2 で用意し、§1.3 の c2 差し替えがそのまま画面の色になるようにする。
- 12 文字は
PERFECT/GOOD/MISSに必要な集合ちょうど(初出順P E R F C T G O D M I S)。字形はgen_ui_font_gb.pyの手打ちピクセルマップの index 1 → 2 の置換であり、画像変換は一切しない(§0)。両フォントの字形が食い違うことは原理的に起こらない - 空白
$73も bank1 側に要る。 BG 属性 bit3 はタイルセル単位なので、判定行 7 セル(row 5 col 6-12)は丸ごと bank1 に張り付いており、" GOOD "/" MISS "の余白だけ bank0 へ戻すことはできない - 末尾に置く理由: かなフォントが増えても判定グリフを押し出せず、
KANA_TILE_LIMITに先にぶつかってビルドが落ちる - 常駐なので毎フレームの転送はない。
judge_font_load(src/judge.asm)が起動時の LCD OFF 窓で 208 B を 1 回 GDMA するだけで、判定文字の描画は VRAM キューを 1 バイトも増やさない - 判定行の BG 属性は bit3 = 1 + BGP2(
$0A)。tools/gen_stage_bg_gb.pyのBANK1_CELLSがこの 7 セルを宣言し、attrmap 生成時に「宣言したセルとちょうど一致する」ことを検算する
2.3 OBJ タイル(VRAM bank0 $8000-$8FFF、256 タイル)
| index | 内容 | タイル | バイト |
|---|---|---|---|
$00-$CF | アイ ダンス 26 フレーム × 8 | 208 | 3,328 |
$D0-$DF | アイ ゲームオーバー 2 フレーム × 8 | 16 | 256 |
$E0-$EB | ノーツ振りアイコン 6 種(8×16 = 2 タイル) | 12 | 192 |
$EC-$ED | 判定マーカー ◎(8×16) | 2 | 32 |
$EE-$F1 | 紙吹雪 2 形状(8×16) | 4 | 64 |
$F2-$FF | 予備(SE エフェクト等) | 14 | 224 |
| 使用 | 242 / 256(95%) | 3,872 |
2.4 OBJ タイル(VRAM bank1 $8000-$8FFF、256 タイル)
| index | 内容 | タイル | バイト |
|---|---|---|---|
$00-$FF | 講師 32 フレーム × 8(常駐) | 256 | 4,096 |
| 使用 | 256 / 256(100%) |
⚠ bank1 は満杯である。講師のフレームを増やす余地はない。 追加演出が要るときは X フリップ属性で idle コマを共有するか、BG 側で作る。
2.5 ART 構成のタイル
⚠ ART では BG が $9000 を索引できない(LCDC.4 = 1 なので index $00-$FF は $8000-$8FFF)。したがって GAME 用の UI 英数フォント(bank0 $9000 常駐)は使えない。ART のテキスト系タイルはすべて VRAM bank1 に置き、BG 属性 bit3 = 1 で索引する。bank0 は一枚絵専用。
| バンク | index | 内容 | タイル |
|---|---|---|---|
| bank0 | $00-$EF | 一枚絵(row 0-11、20×12 = 240 セル) | ≤ 240 |
| bank0 | $F0-$FF | 未使用(予備) | 16 |
| bank1 | $00-$2E | UI 英数フォント 47(§2.1 の $00-$2E と同一データ: 空白 / A-Z / 0-9 / 記号 8 / ▼ ▶) | 47 |
| bank1 | $2F | 区切り線(row 12) | 1 |
| bank1 | $30-$6B | かなフォント(そのシーンで使う集合。プロローグは 60、タイトルは 0) | ≤ 60 |
| bank1 | $6C-$FF | 未使用(予備) | 148 |
| bank1 使用 | 108 / 256(42%) |
⚠ bank0 に英数 16 タイルを切り出す旧案は廃止した。 タイトルのメニュー(START / CONTINUE / PROLOGUE / MODE:NORMAL)に必要なユニークグリフは 16 種(: A C D E G I L M N O P R S T U)で、カーソル ▶・空白・区切り線を含めると 19 種になり、16 タイル枠に入らない。bank1 に UI 英数フォント 47 を丸ごと置けば、メニュー・PUSH A・数字・記号がすべて索引でき、bank0 の 240 タイル割当を 1 タイルも削らずに済む。
⚠ bank1 の $8000-$8FFF は GAME 構成では講師 CHR 256 タイルが常駐している領域である(§2.4)。ART は OBJ OFF なので上書きしてよく、GAME 復帰時に architecture.md §5.4 の手順 4 で再ロードする。
ART 入場時の bank1 GDMA: 英数 47(752 B / 47 ブロック)+ 区切り線 1(16 B / 1 ブロック)+ かな 60(960 B / 60 ブロック)= 1,728 B / 108 ブロック。
3. NES 2bpp → GB 2bpp 変換
核心: NES CHR と GB 2bpp は同じ 16 バイト/タイルで、ビットプレーンの並べ方だけが違う。情報の損失はゼロ。
NES: [p0_y0 .. p0_y7][p1_y0 .. p1_y7] プレーン単位でまとめる
GB : [p0_y0, p1_y0][p0_y1, p1_y1] ... 行単位で lo, hi 交互# tools/gbgfx.py
def tile_nes_to_gb(t16: bytes) -> bytes:
"""NES CHR 1 タイル(16 B)→ GB 2bpp 1 タイル(16 B)。可逆。"""
p0, p1 = t16[:8], t16[8:]
return bytes(b for y in range(8) for b in (p0[y], p1[y]))
def gb_2bpp(tile) -> bytes:
"""8×8 のピクセル値配列(各 0..3)→ GB 2bpp 16 B。"""
out = bytearray()
for row in tile:
lo = hi = 0
for x in range(8):
lo |= (row[x] & 1) << (7 - x)
hi |= ((row[x] >> 1) & 1) << (7 - x)
out += bytes((lo, hi))
return bytes(out)2 つの経路を両方持ち、一致をテストで縛る:
| 経路 | 用途 | ツール |
|---|---|---|
A: 既存 .chr のバイト置換 | NES 版のアートをそのまま持ち込む | nes_chr_to_gb.py |
| B: ピクセルマップからの再生成 | アートの手直し | gen_ai_sprite_gb.py ほか |
⚠ tools/test_chr_equivalence.py が「B の出力 == A(NES 版 .chr) の出力」を検査する。 これが green である限り、NES 側のアートと GB 側のアートがビット単位で同一であることが機械的に証明される。アイの CHR は「バイト不変が原則」という NES 版の申し送りを、変換の決定性で担保する。
⚠ 等価性テストは「ビットプレーン変換のみ」の段階に張る。 8×16 のペア並べ替えとインデックス再マップ(§4)は情報を変えるので、等価性テストの後段に置き、別テスト(並べ替えの全単射性 / 再マップの全単射性・使用 index ≤ 3)で縛る二段構えにする。
SHA-256 ロック: アイ 26F(3,328 B)と講師 32F(4,096 B)の GB 出力に対し GB_LOCKED_SHA256 を unittest で固定する(NES 版の SHA ロック文化の継承)。
4. 8×16 ペア並べ替えとインデックス再マップ
4.1 タイル並べ替え
アイ / 講師 1 フレーム = 16×32 px = 2 列 × 4 行の 8×8 タイル 8 枚。NES ジェネレータは row-major で出力する:
NES 順: 0=(x0,y0) 1=(x1,y0) 2=(x0,y1) 3=(x1,y1) 4=(x0,y2) 5=(x1,y2) 6=(x0,y3) 7=(x1,y3)GB の 8×16 OBJ は「1 OBJ = 縦 2 タイル連続」を要求するので column-major ペア順に並べ替える:
GB_TILE_ORDER = (0, 2, 4, 6, 1, 3, 5, 7)
# GB tile f*8+0, +1 = 左列 上16px (OBJ 0)
# GB tile f*8+2, +3 = 左列 下16px (OBJ 1)
# GB tile f*8+4, +5 = 右列 上16px (OBJ 2)
# GB tile f*8+6, +7 = 右列 下16px (OBJ 3)⚠ 8×16 モードではタイル index の bit0 が無視される。 上表の 4 組はすべて偶数 index から始まるので条件を満たす。フレームを 8 タイル境界に置くこと自体が不変条件であり、ASSERT で強制する。
4.2 帯パレット再マップ(3 帯 → 2 帯)
ピクセルマップは 1 ビットも変更しない。 タイル行ごとにピクセル値(index)を付け替えるだけで、失われた 1 帯分の色を吸収する。
# NES pal1 (タイル行 2): 1=黒 2=白 3=ピンク
# NES pal2 (タイル行 3): 1=ピンク 2=白 3=水色
# GB OBP1 : 1=C_NAVY 2=C_PINK 3=C_WHITE
REMAP = {
0: (0, 1, 2, 3), # タイル行 0 → OBP0 そのまま
1: (0, 1, 2, 3), # タイル行 1 → OBP0 そのまま
2: (0, 1, 3, 2), # タイル行 2 → OBP1: 黒→1(navy) 白→3 ピンク→2
3: (0, 2, 3, 1), # タイル行 3 → OBP1: ピンク→2 白→3 水色→1(navy = 靴)
}⚠ これが 8×16 モード採用の唯一のコストである。 代替案(8×8 OBJ モードで 3 帯を維持)はキャラ 1 体 = 8 OBJ になり、ノーツも 8×8 でレーンの見栄えが落ちる。per-line はどちらも余裕(レーン 7 ≤ 10、キャラ帯 4 ≤ 10)なので、再マップ表 1 個で済む 8×16 を採る。
unittest:
REMAPの各行が全単射であること- 再マップ後の各タイルが非ゼロ index を 3 種類以内しか使わないこと
GB_TILE_ORDERが(0,2,4,6,1,3,5,7)の順列であること
⚠ 下半身の見た目は機械では判定できない。子 issue 3 のスクリーンショット golden をレビュー担当(Fable)がプレビューを目視承認するまで先へ進めない。
5. 一枚絵パイプライン
5.1 全体の流れ
assets/concept/*.png(AI 生成コンセプトアート、1000-1600 px)
│
│ ① クロップ・構図決め(人手、1 回だけ)
▼
assets/art/src/<name>.png
│
│ ② tools/quantize_gbc.py(オフライン、1 回だけ)
▼
assets/art/<name>.png ← ★ 32 色インデックス PNG。これが正典。コミットする
assets/art/<name>-preview.png ← 4 倍拡大レビュー用
assets/art/<name>-report.txt ← 使用パレット数 / ユニークタイル数 / PSNR
assets/art/<name>.gbcpal ← RGB555 LE 64 B。rgbgfx -c gbc: が読む
│
│ ③ 人間レビュー(必須ゲート)
▼
│ ④ rgbgfx -c gbc:assets/art/<name>.gbcpal(毎ビルド、決定的)
▼
build/art/<name>.2bpp / .tilemap / .attrmap / .pal⚠ 出力ファイル名はこの 4 つで確定である。 quantize_gbc.py の実装とこの表が食い違うと、「ツールを走らせてもビルドが読むファイルができない」という無症状の断絶になる。--out-dir を変えてもbasename の付け方は変わらない。
⚠ ② はビルドから完全に外れている。 quantize_gbc.py は make から呼ばれない。ビルドが触るのは ④ だけで、入力の PNG は既に「1 タイル ≤ 4 色・全体 ≤ 8 パレット」を満たしているため、rgbgfx は減色をせず並べ替えるだけになる。これにより「自動画像変換禁止」ルールとの折り合いがつく。
5.2 quantize_gbc.py の CLI と処理
$ python3 tools/quantize_gbc.py --in assets/art/src/title.png \
--out-dir assets/art --name title [--crop X,Y,W,H]| 引数 | 必須 | 意味 |
|---|---|---|
--in PATH | ✓ | 入力 PNG(任意解像度)。既定は assets/art/src/<name>.png |
--out-dir DIR | 出力先ディレクトリ。既定は assets/art | |
--name NAME | 出力の basename。省略時は --in の stem | |
--crop X,Y,W,H | クロップ矩形(元画のピクセル座標)。省略時は 5:3 のセンタークロップ | |
--help | 使い方を表示して exit 0 |
出力(すべて <out-dir>/<name>… で、規約は §5.1 の図と同一):
<name>.png 160×96、インデックスカラー、PLTE 32 色(8 パレット × 4 色)
<name>-preview.png 4 倍最近傍拡大(人間レビュー用)
<name>-report.txt 使用パレット本数 / ユニークタイル数 / 1 タイル最大色数 / PSNR / MSE / パレット一覧
<name>.gbcpal RGB555 LE 64 B(rgbgfx -c gbc: が読む明示パレット)
処理:
1. クロップ(`--crop` 指定、なければ 5:3 センタークロップ)→ Lanczos で 160×96 にリサイズ
2. 全画素を 5bit/ch(32 段階)へ丸める ← GBC の色空間
3. 8×8 タイル 240 個に分割
4. 各タイル内で重み付き k-means(k = 4、重み = その色の画素数、距離 = RGB555 空間のユークリッド距離)
→ 4 色に削減。誤差拡散はしない(タイル境界に色が漏れるため)
5. 240 個のタイルを**タイル平均色**の重み付き k-means(k = 7、ユークリッド距離)で 7 グループに分ける
6. 各グループの最終 4 色を、そのグループに属する全タイルの色を重み付き k-means(k = 4)にかけて決める
7. 各タイルの画素を、割り当てられたグループパレットの最近傍色へ再マップ
(1 タイル ≤ 4 色・全体 ≤ 7 パレットが構造的に保証される。再試行ループは持たない)
8. **パレット 7(index 28-31)はテキスト帯用の固定色**(`$0000` / `$7FFF` / `$3082` / `$20C6`)を書き込む
9. インデックス PNG(PLTE = 8×4 = 32 色)と `.gbcpal` 64 B を書き出す⚠ 決定的である。 k-means の初期化は固定シード(タイル index / グループ番号から導出)で、乱数源を外部から受け取らない。同じ入力に対して同じ出力を返すことを test_quantize_gbc.py が 2 回実行のバイト比較で検査する(この検査だけは numpy / Pillow を要するので skipUnless で括る。§6.1 / §8)。
⚠ 上の記述は実装(tools/quantize_gbc.py)と 1:1 で対応させること。 旧稿は「median-cut」「Hausdorff 距離」「誤差超過タイルの 3 回再試行」「N_PALETTES = 8」と書いていたが、実装は重み付き k-means・ユークリッド距離・再試行なしであり、しかも 8 本すべてを絵に使っていた。8 本すべてを絵に使うと §1.7 の「BGP7 はテキスト帯用に固定予約」が崩れ、下部 3 行のかなテキストが絵のパレットで塗られて読めなくなる。
⚠ 絵は 160×96(row 0-11)である。 160×144 全画面にすると 360 セルとなりテキストを重ねる場所がなくなる。原典のテキスト演出(下部 3 行)を維持するための制約。
5.3 rgbgfx 呼び出し
rgbgfx assets/art/title.png \
-c gbc:assets/art/title.gbcpal \ # 量子化ツールが出力した明示パレット(減色しない)
-u -m \ # ユニーク化 + X/Y 反転一致の統合
-n 8 -s 4 \ # パレット 8 本 × 4 色(超えたらエラー)
-N 240 \ # ★ タイル数上限 240。超えたらエラー = ビルドが落ちる
-b 0 \ # ベースタイル ID = 0(bank0 $00-$EF)
-o build/art/title.2bpp \
-p build/art/title.pal \
-t build/art/title.tilemap \
-a build/art/title.attrmap⚠ -N 240 は「-u -m 後のユニークタイル数の上限」であり、画像サイズ違いは検出しない(§5.4)。Makefile には必ず入れるが、これを寸法チェックとして当てにしてはならない。タイル数超過は test_art_assets.py の .2bpp ≤ 240 × 16 = 3,840 B で、寸法は同テストの PNG 160×96 検査と .tilemap / .attrmap 各 240 B 検査で受け持つ。
⚠ フラグ名は RGBDS 1.0.3 の man rgbgfx で検証してから使う。 ⚠ -c embedded は「PNG の先頭 4 色を全体に強制する」DMG 向け仕様で GBC 8 パレットには使えない(2026-09-02 検証済み)。-c gbc:<file>(RGB555 LE 64 B)または -n 8 -s 4 自動生成を使う。使用版を docs/build.md に固定明記する。
5.4 ROM バンクが足りなくなった場合の逃げ道(優先順)
⚠ 「ユニークタイルが 240 を超える」ことは原理的に起きない。 一枚絵は 160×96 = 240 セルしかないので、-u -m を通したユニークタイルは必ず 240 以下になる。
⚠ -N 240 は画像サイズ違いを検出しない。 -N が制限するのは -u -m 後のユニークタイル数であってセル数ではないので、160×144 の PNG を誤って渡してもユニークタイルが 240 以下なら素通りし、360 B の .tilemap を吐く(RGBDS 1.0.3 で実測: 160×144 の単色 PNG は unique 1 枚・exit 0・.tilemap 360 B)。この 360 B は §2.1 / 下記手順 4 が前提とする「CPU が 12 行 × 20 B をコピー」を壊し、無症状で画面が崩れる。画像サイズ 160×96 は 2 段で検査する: (a) quantize_gbc.py が出力 PNG を必ず 160×96 で書き、test_quantize_gbc.py がそれを検査する、(b) test_art_assets.py がコミット済み PNG の 160×96 と .tilemap / .attrmap の各 240 B を検査する。-N 240 はこの 2 段に対する補助(ユニークタイル数の上限)として付ける。
実際に詰まるのは ROM バンクの残余である(bank6 = 3.2 KB / bank7 = 1.5 KB。architecture.md §2.2):
- 絵を左右対称寄りに描き直す(
-mの反転統合が効き、.2bppが 3,840 B より小さくなる) - 上部 2 行を単色グラデーション帯にして 40 セルを 2 タイル以下に圧縮する
- ROM を 256 KB に上げ(
$0148 = $03)、絵を追加バンクに分ける。MBC5 なのでコード変更は不要
5.5 一枚絵 5 枚
| # | シーン | 元画(assets/concept/) | ROM バンク |
|---|---|---|---|
| 1 | タイトル | ai-game-package-v1.png(1448×1086) | 6 |
| 2 | プロローグ p1 | ai-character-concept-v1.png(1003×1568) | 6 |
| 3 | プロローグ p2 | ai-turnaround-v1.png(1635×962)を加工 | 6 |
| 4 | プロローグ p3 | ai-dance-instructor-concept-v2.png(1586×992) | 7 |
| 5 | プロローグ p4 | ai-dance-sprite-nes-v4-sheet.png の 1 ポーズを拡大 | 7 |
1 枚あたりの ROM 消費 = タイル 3,840 + tilemap 240 + attrmap 240 + パレット 64 = 4,384 B(上限)。
実測(issue #12 / #13。rgbgfx -u -m の反転統合後):
| 名前 | ユニークタイル | .2bpp | ROM 消費(+ 544 B) | バンク |
|---|---|---|---|---|
title | 211 | 3,376 B | 3,920 B | 6 |
prologue1 | 208 | 3,328 B | 3,872 B | 6 |
prologue2 | 233 | 3,728 B | 4,272 B | 6 |
prologue3 | 206 | 3,296 B | 3,840 B | 7 |
prologue4 | 195 | 3,120 B | 3,664 B | 7 |
合計 19,568 B(bank6 12,064 B / bank7 7,504 B)。5 枚とも 128 KB ROM の bank6/7 に収まったので $0148 = $03(256 KB)への引き上げは不要である(architecture.md §11 の未決 A4、issues.md I3 の解消)。⚠ prologue2 は PSNR 18.88 dB で 5 枚中いちばん低く、ユニークタイルも 233 / 240 と余裕が 7 しかない。 描き直すときはこの 2 つを先に見ること。
⚠ rgbgfx -t / -a は入力画像のタイル数ぶんしか出力しない。 160×96 なら 20 × 12 = 240 B であり、行末を 32 列へパディングしない。したがって 384 B を前提にした ROM 見積・GDMA ブロック数はすべて誤りである。 tilemap / attrmap を VRAM へ運ぶ方法(GDMA ではなく CPU の 12 行 × 20 B コピー)は architecture.md §2.1 / §5.4 に定める。
6. ジェネレータ一覧(tools/)
| ツール | 入力 | 出力 | 備考 |
|---|---|---|---|
gbgfx.py | — | ライブラリ | GB 2bpp エンコーダ + 8×16 ペア並べ替え + タイル重複排除。全ジェネレータが import |
nes_chr_to_gb.py | vendoring 済み assets/nes/ai-dance-sprite-nes-v4*.chr | GB 2bpp | ビットプレーン置換のみ。決定的 |
gen_ai_sprite_gb.py | tools/ai_frames.py(NES tools/gen_ai_sprite.py の NES_RGB〜ANIMATION_GROUPS、26-904 行を vendoring) | assets/ai-26f.2bpp(3,328 B) / ai-gameover.2bpp(256 B) / src/ai_sprite_data.inc / preview PNG | SHA-256 ロック継承。NES CHR 経路は assets/nes/ai-dance-sprite-nes-v4*.chr |
gen_coach_sprite_gb.py | NES gen_instructor_sprite.py の 32 フレーム | assets/coach-32f.2bpp(4,096 B) / src/coach_sprite_data.inc | |
gen_stage_bg_gb.py | 手打ちタイルマップ(新規、20×18 用) | stage.2bpp(2,048 B) / stage-{play,dialog,select,ending}.tilemap(各 576 B) / .attrmap(各 576 B) / src/stage_bg_data.inc | 観客テーブル 11,424 B は出力しない(ランタイム合成) |
gen_ui_font_gb.py | 手打ち | uifont.2bpp(47 タイル = 752 B) | A-Z 0-9 記号 8 + ▼ ▶。全グリフがパレット index 1 |
gen_judge_font_gb.py | gen_ui_font_gb.py の手打ちピクセルマップ(index 1 → 2 置換) | judgefont.2bpp(13 タイル = 208 B)/ src/judge_font_data.inc | 判定グリフ。空白 1 + P E R F C T G O D M I S 12 をパレット index 2 で描く(§2.2)。VRAM bank1 $73-$7F に常駐し、KANA_TILE_LIMIT = 115 も出力する |
gen_kana_font_gb.py | NES 版の生成済み .chr 3 本(dialog 54 / prologue 60 / ending 76)+ JSON マッピング | kana-dialog.2bpp(864 B) / kana-prologue.2bpp(960 B) / kana-ending.2bpp(1,216 B) / src/kana_data.inc | 美咲 BDF が手元にないため、NES 版の成果物を 2bpp 変換して流用する。TIKTOK→SNS の置換に必要な S N は UI 英数フォント側のグリフを使う |
gen_notes_gb.py | 手打ち | notes.2bpp(ノーツ 6 + マーカー 1 + 紙吹雪 2 = 18 タイル = 288 B) | 8×16 前提で上下 2 タイル |
gen_palettes.py | (R,G,B) 定義(Python 直書き) | src/palette_data.inc + 本書の表 | 15bit 値を機械生成する。手打ち禁止 |
gen_noise_table.py | — | noise_table()(16 × 1 B、Python 関数。ライブラリとして snd_tables_gb.py が import) | f = 262144 / (r × 2^s) で最良の NR43 を全探索。単体では .inc を書き出さない |
snd_tables_gb.py | gen_noise_table.noise_table() | note_div_table() / vol_mul_table() / drum_table_bytes() 等(Python 関数群) | note_div[](84×2 B, div = round(131072/f)) / vol_mul[](256 B) / ドラム表(128 B, kick 12f / snare 9f / tom 11f) の共有ロジック。旧 gen_period_table.py / gen_drum_tables.py はここに統合され廃止(export_bgm_gb.py と ref_sim_gb.py の二重管理を避けるため) |
export_bgm_gb.py | assets/bgm/new_*_dpcm.json(NES と同一ファイル)+ snd_tables_gb.py の表 | src/bgm_data.inc(note_div / vol_mul / noise_nr43 / drum_table / drum_offset / drum_length / BGM ストリーム / 曲テーブルを 1 本にまとめて出力。src/instruments.inc は生成しない) | ストリーム形式は流用。dm を drum id に、周期を div に。CH1/CH2 < 64 Hz、CH3 < 32 Hz を警告 + 1 オクターブ上げ。vol 系列の定常部を平坦化。ドラム表は ROMX BANK[1]、note_div/vol_mul は ROM0(issue #7 U1) |
export_charts_gb.py | assets/charts/charts.json(NES と同一)+ BGM JSON | src/charts_data.inc | GB_FPS と NOTE_APPROACH_FRAMES=96 |
reflow_text.py | テキスト定義(Python 直書き、逐語) | src/text_data.inc | 20 タイル / 4 行の検査つき |
quantize_gbc.py | assets/art/src/<name>.png | assets/art/<name>.png + -preview.png + -report.txt + .gbcpal | ビルドから呼ばれない。オフライン 1 回のみ。numpy + Pillow が要る(§6.1) |
ref_sim_gb.py | BGM JSON | 期待 APU シャドウ列 | E2E オラクル(e2e.md §5) |
test_*.py | — | unittest | e2e.md §6 |
gen_ai_sprite_gb.py のプレビュー PNG は --preview を指定したときだけ生成する(既定の生成経路は標準ライブラリのみ)。
⚠ かなフォントは美咲ゴシック BDF がリポジトリ外にあり、手元にない。 NES 版が既に生成済みの .chr(54 / 60 / 76 タイル)と JSON マッピングをビットプレーン変換して流用する。ライセンス表記 assets/fonts/LICENSE-misaki.txt も一緒に持ち込む。
6.1 Python の依存
| ツール | 依存 |
|---|---|
quantize_gbc.py の減色実行(quantize()) | numpy / Pillow |
quantize_gbc.py のCLI(--help / 引数解析) | 標準ライブラリのみ |
図ジェネレータ(issues.md #15) | Pillow |
上記以外のジェネレータ・test_*.py | 標準ライブラリのみ |
python3 -m venv .venv
.venv/bin/pip install -r tools/requirements.txt # numpy, Pillow
.venv/bin/python tools/quantize_gbc.py --help⚠ import numpy / from PIL import Image をモジュール先頭に置いてはならない。 両者は _load_deps() の中で遅延 import し、quantize() の冒頭からだけ呼ぶ。これにより python3 tools/quantize_gbc.py --help は numpy / Pillow が 1 つも入っていないクリーンなクローンでも exit 0 になる(issues.md #1 受入条件 7)。型注釈が import 前に評価されないよう、ファイル先頭に from __future__ import annotations を置く。依存が欠けたまま減色を実行した場合は pip install -r tools/requirements.txt を促して SystemExit する。
⚠ tools/requirements.txt をリポジトリに置く(内容は numpy と Pillow の 2 行)。make / make test / make e2e は quantize_gbc.py の減色を実行しないので、ビルドとテストは numpy なしで exit 0 になる。 唯一 quantize_gbc.py に触れる test_quantize_gbc.py は、CLI 検査(依存不要)と減色再現性検査(unittest.skipUnless で numpy / Pillow がある環境だけ実行)に分かれている(§8)。依存が要るのは一枚絵の減色をやり直すときだけである。導入手順は docs/build.md(issues.md #16)にも書く。
7. Makefile 依存
RGBASM ?= rgbasm
RGBLINK ?= rgblink
RGBFIX ?= rgbfix
RGBGFX ?= rgbgfx
BUILD := build
ROM := $(BUILD)/aidopagaki.gbc
SYM := $(BUILD)/aidopagaki.sym
MAP := $(BUILD)/aidopagaki.map
SRC := $(wildcard src/*.asm)
OBJ := $(patsubst src/%.asm,$(BUILD)/%.o,$(SRC))
ART := title prologue1 prologue2 prologue3 prologue4
all: $(ROM)
# --- 自動生成される .inc(NES 版は 2 ルールだけだった。GB 版は複数ルールに拡張する) ---
# ⚠ issue #7: note_div / vol_mul / noise_nr43 / drum_table は個別の .inc を持たず、
# snd_tables_gb.py(+ gen_noise_table.py を import)のロジックで export_bgm_gb.py が
# src/bgm_data.inc に一括出力する(旧 gen_period_table.py / gen_drum_tables.py / 個別
# instruments.inc は廃止。ref_sim_gb.py と表を二重管理しないため)。
src/bgm_data.inc: tools/export_bgm_gb.py tools/snd_tables_gb.py tools/gen_noise_table.py $(BGM_JSON)
python3 tools/export_bgm_gb.py
src/charts_data.inc: tools/export_charts_gb.py tools/export_bgm_gb.py \
tools/snd_tables_gb.py assets/charts/charts.json $(BGM_JSON)
python3 tools/export_charts_gb.py
src/palette_data.inc: tools/gen_palettes.py
python3 tools/gen_palettes.py
src/text_data.inc: tools/reflow_text.py
python3 tools/reflow_text.py
src/ai_sprite_data.inc assets/ai-26f.2bpp assets/ai-gameover.2bpp: tools/gen_ai_sprite_gb.py tools/gbgfx.py tools/ai_frames.py
python3 tools/gen_ai_sprite_gb.py
src/judge_font_data.inc assets/judgefont.2bpp: tools/gen_judge_font_gb.py \
tools/gen_ui_font_gb.py tools/gbgfx.py
python3 tools/gen_judge_font_gb.py
src/stage_bg_data.inc assets/stage.2bpp: tools/gen_stage_bg_gb.py \
tools/gen_ui_font_gb.py tools/gen_judge_font_gb.py tools/gbgfx.py
python3 tools/gen_stage_bg_gb.py
# --- 一枚絵(rgbgfx はタイル上限を -N で強制。減色はしない) ---
$(BUILD)/art/%.2bpp: assets/art/%.png assets/art/%.gbcpal
@mkdir -p $(BUILD)/art
$(RGBGFX) -c gbc:assets/art/$*.gbcpal -u -m -n 8 -s 4 -N 240 -b 0 \
-o $@ -p $(BUILD)/art/$*.pal \
-t $(BUILD)/art/$*.tilemap -a $(BUILD)/art/$*.attrmap $<
$(BUILD)/%.o: src/%.asm
$(RGBASM) -h -o $@ $<
$(ROM): $(OBJ) $(addprefix $(BUILD)/art/,$(addsuffix .2bpp,$(ART)))
$(RGBLINK) -n $(SYM) -m $(MAP) -o $@ $(OBJ) # ★ -n で .sym を出す(E2E の生命線)
$(RGBFIX) -v -C -m 0x19 -p 0xFF -t "AIDOPAGAKI" -i "ADPJ" -k "AE" -l 0x33 $@
debug:
$(MAKE) RGBASM_FLAGS="-D DEBUG=1 -D DEBUG_MENU=1" all
.PHONY: all debug test e2e clean run
test: # Python 生成物の unittest
python3 -m unittest discover -s tools
e2e: $(ROM) # PyBoy E2E
python3 -m pytest tests -q
run: $(ROM)
$(SAMEBOY) $(ROM)⚠ gb サブフォルダは 1 つも作らない。 本リポジトリは GB 専用なので、tools / tests / build / src の下に gb ディレクトリを掘るのは誤りである。検出コマンドは issues.md §4 の GB_SUBDIR_GREP を正とする。⚠ tools/gbgfx.py や docs/gb-package.md のような gb で始まる正当なファイル名は検出対象ではない(ディレクトリ指定だけを見る)。
⚠ 生成物(.inc / .2bpp / インデックス PNG)はすべてコミットする。 Python がなくてもビルドできるのは NES 版の方針であり、これを継承する。ただし自動生成ルールは NES の 2 本から 9 本に増やし、「ジェネレータを直したのに再生成し忘れる」事故を潰す。
⚠ rgblink -n $(SYM) を落とすと E2E が全滅する。 e2e ターゲットは $(SYM) の存在を先頭でチェックする。
⚠ NES 版「鉄則 #6」を継承: make が通っても make debug が壊れることがある。 CI では両方を exit code で確認する。
8. アセット関連の unittest
| ファイル | 件数目安 | 内容 |
|---|---|---|
test_gbgfx.py | 8 | 2bpp ラウンドトリップ、既知ベクタ 3 種(市松 / 全 0 / 全 3)、GB_TILE_ORDER が順列、REMAP が全単射 |
test_chr_equivalence.py | 4 | ジェネレータの GB 出力 == nes_chr_to_gb.py(NES .chr) の出力、SHA-256 ロック |
test_ai_sprite_gb.py | 5 | .2bpp が 3,328 B、再生成でバイト一致、フレーム数 26+2、再マップ後の使用 index ≤ 3 |
test_coach_sprite_gb.py | 4 | 4,096 B、32 フレーム、bank1 に収まる |
test_stage_bg_gb.py | 9 | .2bpp 長 %16、tilemap 576 B、attrmap 576 B、BG タイル総数 ≤ 128、max(tilemap) < 128、属性のバンクビットが判定 7 セルとちょうど一致、bank1 セルが判定グリフ index を指す |
test_judge_font_gb.py | 10 | 13 タイル = 208 B、グリフ集合が PERFECT/GOOD/MISS にちょうど必要な 12 文字、全グリフがパレット index 2 のみ、空白タイルが真っ白でなく空、字形が UI 英数フォントと一致、$73-$7F が bank1 BG 窓の末尾、KANA_TILE_LIMIT = 115 ≥ 76、再生成でバイト一致 |
test_palettes.py | 4 | 15bit 値が (R,G,B) から再計算した値と一致、全 BG パレットの c1 == $7FFF、OBJ パレットが 8 本以内 |
test_art_assets.py | 8 | コミット済みの成果物だけを検証する(減色は 1 度も実行しない → numpy / Pillow 不要): インデックス PNG が 160×96、PLTE ≤ 32 色、1 タイル ≤ 4 色、絵が使うパレットが 0-6 の 7 本以内(§1.7 の BGP7 予約を守る)、.gbcpal が 64 B でその末尾 8 B が固定の BGP7、.2bpp ≤ 3,840 B、.tilemap / .attrmap が各 240 B、rgbgfx exit 0、5 枚すべて |
test_quantize_gbc.py | 4 | CLI 検査 1 件は依存なしで必ず走る: python3 tools/quantize_gbc.py --help が exit 0(import quantize_gbc 自体も numpy なしで成功する)。残り 3 件は @unittest.skipUnless(HAVE_DEPS, ...) で numpy / Pillow がある環境だけ実行: 同じ入力で 2 回実行した出力 4 種がバイト一致、出力 PNG が 160×96 で 1 タイル ≤ 4 色、.gbcpal が 64 B で末尾 8 B が BGP7 |
test_kana_font_gb.py | 6 | 3 種のタイル数 54/60/76、全文字にグリフ、かな窓 ≤ KANA_TILE_LIMIT = 115(末尾 13 タイルは判定グリフの常駐領域、§2.2)、flow_enter_ending が入替を呼ぶ(静的解析) |
test_text_pages.py | 6 | 全行 ≤ 20 タイル / 1 ページ ≤ 4 行 / 逐語一致 / グリフ網羅 / TIKTOK 不在 / SNS 存在 |
test_export_charts_gb.py | 33 | speed88 が 1911/1792/1977/1509/2389、target[0] == 96、単調増加、ceil 式の全 N 再計算一致、16bit 桁あふれなし、$FF 終端(本体に混入なし)、moves が 0-6、拍数・ノーツ数・内訳が §7.3 の表と一致、CHARTED_SONGS_MASK == %00001111、実時間 ±0.05 s、EASY が焼き込まれていない(原本 charts.json とバイト一致)、CHART_BANK == BGM_BANK、生成物の EQU 行が全て DEF 付き、NOTE_APPROACH_FRAMES / CHARTED_SONGS_MASK を再定義せず ASSERT で固定、コミット済み .inc が再計算と一致、曲順が export_bgm_gb.ORDER と一致、絶対パス 0 件 |
test_export_bgm_gb.py | 8 | ストリーム長、dedup、CH1/CH2 の div ≤ 2047、CH3 の div ≤ 4095、下限周波数の警告、vol 平坦化、曲と譜面が同一バンク |
test_period_table.py | 3 | 84 エントリ、全 div が round(131072/f) と一致、CH3 が div>>1 で成立 |
test_noise_table.py | 3 | 16 エントリ、f = 262144/(r×2^s) で再計算した値と一致、上限が 524,288 Hz |
test_drum_tables.py | 6 | 128 B、kick 12f / snare 9f / tom 11f、最終 CH4 フレームが NR42 = $00、CH3 X が 2048 - round(65536/f)、4 B のビット割当(architecture.md §7.7)でエンコード・デコードがラウンドトリップする、CH3_RELEASE が各ドラムでちょうど 1 フレームだけ立ち、位置が KICK = f11 / SNARE = f2 / TOM = f10 |
| 合計 | 121 | NES 版 46 件を上回る |
⚠ 減色の再現性検査を test_art_assets.py に置いてはならない。 量子化を走らせる検査は test_quantize_gbc.py に隔離し、skipUnless で依存の有無を見る。こうしないと make test = python3 -m unittest discover -s tools が numpy なしのクローンで落ち、§6.1 と共通受入条件 2 が両立しない。
⚠ A4 = 440 Hz を「0.5 セント以内」で検査してはならない。 div = round(131072/440) = 298 は −0.634 セント、隣の 297 は +5.185 セントであり、GB のハードウェアでは 0.5 セントに入れない。周期表の正しさは「div が round(131072/f) と一致すること」で検査する。参考として A4 の誤差が 1.0 セント未満であることを併せて確認する。