0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kiro のだいぼうけん!開発記 〜ファミコンの制約を守りながらKiroとClaudeにシューティングゲームを作らせる〜

0
Last updated at Posted at 2026-05-29

こんにちは。

今回は、番外編!
Webで見かけた、Kiro でゲームボーイソフトを開発するといったものにインスパイアを受けて、「ファミコンで動くシューティングゲーム」を目指した記録です。

この記事は、Kiro(AI搭載IDE)とAnthropic Claude Opus 4.6を用いて開発した記録です。実際にKiroと会話しながら実装し、プレイテストでバグを見つけてはその場で直す、という試行錯誤の過程をそのままお伝えしています。

なお、開発は現在進行中です。HTML5版が遊べる状態になったので、ここまでの記録を残しておきます。

開発環境

Kiro + Claude Opus 4.6

KiroはAWSが提供するAI搭載IDEです。バックエンドのLLMとしてAnthropic Claude Opus 4.6を使用しています。

今回の開発で特に活きたのはSpec駆動開発の機能です。要件定義→設計→タスク分解→実装を一貫してAIがサポートしてくれます。ゲーム開発のように「守るべきルール」が大量にあるプロジェクトでは、要件定義の段階でルールを明文化しておくとAIが逸脱しにくくなります。

テスト環境

ツール 用途
Vitest テストランナー
fast-check プロパティベーステスト

最終的に820件のテストが全パスする状態で開発を進めました。

なぜファミコンなのか

前述の通り、ゲームボーイを開発しているという記載を見かけたから、ことの発端。
作るなら、やっぱりファミコンだよねということで選びました。

しかし、ファミコン(NES)のゲーム開発には独特の制約があります。

  • 解像度: 256×240ピクセル
  • 同時表示色: 最大25色(背景13色 + スプライト12色)
  • スプライト: 画面上に最大64個
  • 1スキャンラインあたり: 8スプライトまで

この制約の中でグラディウスやツインビーが動いていたと思うと、当時の開発者の技術力に頭が下がります。

「この制約を守ったゲームを、AIに作らせたらどうなるか?」

ただし、6502アセンブリをいきなり書かせるのは無理があります。
そこで2段階アプローチを取ることにしました。

  1. HTML5 + JavaScriptでプロトタイプを作る(ファミコン制約に準拠した設計で)
  2. 動作確認後、NES ROMに移植する

つまり、まずはそれっぽいものを開発して、動かすということです。
Phase 1が完了したので、ここまでの記録です。

全体のアーキテクチャ

┌─────────────────────────────────────────────────────────┐
│              Phase 1: HTML5プロトタイプ(完了)            │
├─────────────────────────────────────────────────────────┤
│  HTML5 Canvas (256×240)                                 │
│  JavaScript (ES6 Modules)                               │
│  ファミコンPPU制約に準拠した設計                             │
│  (パレット制限、スプライト制限、8×16モード)                   │
└─────────────────────────────────────────────────────────┘
         ↓ 移植予定
┌─────────────────────────────────────────────────────────┐
│              Phase 2: NES ROM(これから)                 │
├─────────────────────────────────────────────────────────┤
│  6502 Assembly / cc65                                   │
│  CHR-ROM (スプライトデータ)                                │
│  .nes ROMファイル → エミュレータ or 実機                    │
└─────────────────────────────────────────────────────────┘

HTML5版の段階でファミコンの制約を守っておけば、移植時の手戻りを最小化できる...はずです。

ファミコン制約をSpecに落とし込む

Kiroに「ファミコン風の横スクロールシューティングを作りたい」と伝えると、Specの作成が始まります。

ここで重要なのが、ファミコンPPUの制約を要件として明示的に書かせること。

パレット制約

ファミコンPPUは64色のマスターパレットを持ちますが、画面上に同時表示できるのは最大25色。さらに各パレットは3色+透明(または共通背景色)の4色構成です。

これをコードに落とすとこうなります:

// $0Dは使用禁止(一部TVでグリッチを起こす)
export const MASTER_PALETTE = [
    [84, 84, 84],      // $00
    [0, 30, 116],       // $01
    // ... 64色分
    null,               // $0D: 使用禁止
    // ...
];

// スプライトパレット4種(各3色+透明)
export const SPRITE_PALETTES = [
    [null, 0x30, 0x0F, 0x16],  // SP0: プレイヤー(白、黒、赤)
    [null, 0x2C, 0x0F, 0x11],  // SP1: 分身(水色、黒、青)
    [null, 0x27, 0x17, 0x07],  // SP2: 敵(オレンジ、茶、暗赤)
    [null, 0x12, 0x16, 0x30],  // SP3: ボス(青、赤、白)
];

$0Dnullにしているのは、nesdev.orgに記載されている実機の既知問題への対応です。HTML5版では見た目に影響しませんが、NES移植時にこのまま使えるようにしておきます。

スプライトバジェット

ファミコンは画面上に最大64スプライトしか表示できません。8×16モードでは1キャラクター(16×16px)に4スプライトを消費します。

設計書でバジェットを定義し、EnemyManagerがランタイムで管理します:

通常ステージ時:
  プレイヤー: 4スプライト
  分身(Option_Clone): 最大8(4×2体)
  弾丸: 最大20
  敵: 最大24(16×16敵6体分)
  パワーアップ: 最大2
  シールド: 4
  合計: 最大62(上限64以内)

コード上では:

const MAX_ENEMY_SPRITES = 24;

spawnEnemy(typeName, isColorVariant, speedMultiplier) {
    const spriteCost = def.size <= 8 ? 1 : 4;
    if (this._currentSpriteCount + spriteCost > MAX_ENEMY_SPRITES) {
        return null;  // バジェット超過 → 生成しない
    }
    // ...
}

上限を超える敵は生成しない。シンプルですが、これでファミコンの制約を守れます。

プロパティベーステストで仕様を形式化する

Kiroが生成したテストの中で特に面白かったのが、fast-checkを使ったプロパティベーステスト(PBT)です。

通常のユニットテストが「この入力に対してこの出力」を検証するのに対し、PBTは「任意の入力に対してこの性質が常に成り立つ」を検証します。

例えば衝突判定:

it('任意の2つの矩形に対して、checkAABBの結果が幾何学的な重なり判定と一致する', () => {
    fc.assert(
        fc.property(arbRect, arbRect, (rectA, rectB) => {
            const actual = CollisionDetector.checkAABB(rectA, rectB);
            const expected = geometricOverlap(rectA, rectB);  // リファレンス実装
            expect(actual).toBe(expected);
        })
    );
});

ランダムな矩形を100回生成して、実装がリファレンス実装と常に一致することを検証します。手書きでは思いつかないエッジケース(幅0の矩形、座標が負の矩形など)も自動的にカバーされます。

パレットの検証もPBTで:

it('isValidPaletteIndex が有効範囲(0〜63、$0D除外)の整数に対してのみ true を返す', () => {
    fc.assert(
        fc.property(fc.integer({ min: -10, max: 73 }), (index) => {
            const result = isValidPaletteIndex(index);
            const expected = index >= 0 && index <= 63 && index !== 0x0D;
            expect(result).toBe(expected);
        })
    );
});

最終的に34個のプロパティを定義しました。テストが仕様書を兼ねる、という感覚です。

プレイテストで見つかったバグたち

テストが全パスしていても、実際にプレイすると問題は見つかります。以下はKiroとの会話で解決したバグの一部です。

ボスが倒せない

症状: ボスのHPゲージが0になっても撃破されない。

原因: CollisionDetectorboss.hp -= damageと直接HPを減算していて、Boss.takeDamage()を経由していなかった。HPが0になってもstateDEFEATEDに遷移しない。

修正: boss.takeDamage(damage)を呼ぶように変更。

CollisionDetectorは汎用的な衝突判定クラスなので、状態遷移のロジックを持つべきではありませんでした。

弾がKiroの頭から出る

症状: 横スクロールシューティングなのに、レーザービームが上方向に飛ぶ。

原因: LaserBeamのコンストラクタにsuper(x, y, 0, -4, 'player', 2)(上方向)がハードコードされていた。横シュー化の際に漏れた箇所。

修正: super(x, y, 4, 0, 'player', 2)(右方向)に変更。

もともと縦シューとして実装されていたものを横シューに方針転換した際の取りこぼしでした。

ボスに安全地帯がある

症状: ボスが上下にサイン波で動くだけなので、画面端に張り付けば被弾しない。

修正: プレイヤーY座標への追従 + 2周波数のサイン波重ね合わせ + X方向の揺れを追加。

// プレイヤーに向かってゆっくり追従(安全地帯を潰す)
const diffY = playerY - (this.y + this.height / 2);
if (Math.abs(diffY) > 4) {
    this.y += Math.sign(diffY) * trackingSpeed;
}
// 2つの周波数を重ねて予測しにくくする
this.y += Math.sin(this._elapsedTime * 2.0) * moveSpeed * 0.5;
this.y += Math.sin(this._elapsedTime * 3.7) * moveSpeed * 0.3;

これらはすべて「Kiroに症状を伝える → 原因特定 → 修正」のサイクルで解決しています。テストが820件あるおかげで、修正が他の箇所を壊していないことをすぐ確認できます。

ゲームの概要

タイトルは「Kiro のだいぼうけん!」。お化けのキャラクター「Kiro」を操作する横スクロールシューティングです。
今の所、キャラクターは、Kiro以外は代償様々な箱型のオブジェクトです。そのうち、いつか、きっとドット絵を描いて取り込みます。

  • 全6ステージ(お家 → オフィス → データセンター → お空 → 宇宙 → 人工衛星)
  • 各ステージにテーマ別の敵とボス
  • 7種類のパワーアップ
  • レーザービームはプログラミング言語の関数テキストが弾として飛ぶ(console.log("Kiro!") とか fmt.Println("Boo!") とか)
  • エンディング後に2周目(難易度上昇)

ステージ6のボス「AI大型コンピュータ」は、発狂モード(HP30%以下)になるとcodeRain弾24発を間隔10フレームで撃ってきます。かなり厳しい。

プレイURL:
http://kiro-ghost-shooter-game.s3-website-ap-northeast-1.amazonaws.com
※期間限定なので、気がついたら消えてるかも。

操作: 矢印キー/WASD で移動、スペースで発射。

今後の予定: NES ROM移植

Phase 2として、このHTML5版をNES ROMに移植する予定です。

移植に向けて、HTML5版の段階で以下の準備が整っています:

項目 ファミコン制約 HTML5版での対応
解像度 256×240 Canvas解像度を256×240に固定
パレット 4色×8パレット Palette.jsで厳密に管理
スプライト数 最大64個 EnemyManagerでバジェット管理
スプライトモード 8×16 16×16キャラ = 4スプライト構成
タイル 8×8グリッド 背景を8×8タイルで構成
$0D禁止 TVグリッチ防止 isValidPaletteIndexで検証

移植にはcc65(6502向けCコンパイラ)を使う予定です。ゲームロジックの大部分はCで書き、パフォーマンスクリティカルな部分(スクロール処理、スプライトDMA転送など)のみアセンブリで書く方針です。

Kiroがどこまで6502アセンブリを書けるか...続報をお待ちください。
ゲームボーイソフトをKiroと開発されている方は、割とサクッと作られているようなので、案外いけるのかも・・・?

学び

  1. Spec駆動開発はゲーム開発と相性が良い。ファミコンの制約のような「守るべきルール」が多い開発では、要件定義段階で制約を明文化しておくとAIが逸脱しにくい
  2. プロパティベーステストは仕様の形式化。「任意の入力に対してこの性質が成り立つ」という記述は、仕様そのもの
  3. プレイテストからのフィードバックが速い。「ボスが弱い」「安全地帯がある」といった感覚的なフィードバックを、その場でKiroに伝えて調整できる
  4. テストが多いと方針転換が怖くない。縦シュー→横シューの変更でも、壊れた箇所をすぐ検出できた
  5. ファミコン制約を先に決めておくと移植が楽になる(はず)。これはPhase 2で検証予定

まとめ

「ファミコンで動くゲームをAIに作ってもらう」という無茶振りから始まったこのプロジェクト、HTML5版は一通り遊べる状態になりました。

Kiro + Claude Opusの組み合わせは、ゲーム開発においても十分に実用的です。特にSpec駆動開発による要件の明文化と、プロパティベーステストによる仕様の形式検証は、ゲームのような「正しさの定義が難しい」ドメインでも有効でした。

次はNES ROM移植。6502との格闘が待っています。

記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?