こんにちは。
今回は、番外編!
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段階アプローチを取ることにしました。
- HTML5 + JavaScriptでプロトタイプを作る(ファミコン制約に準拠した設計で)
- 動作確認後、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: ボス(青、赤、白)
];
$0Dをnullにしているのは、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になっても撃破されない。
原因: CollisionDetectorがboss.hp -= damageと直接HPを減算していて、Boss.takeDamage()を経由していなかった。HPが0になってもstateがDEFEATEDに遷移しない。
修正: 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と開発されている方は、割とサクッと作られているようなので、案外いけるのかも・・・?
学び
- Spec駆動開発はゲーム開発と相性が良い。ファミコンの制約のような「守るべきルール」が多い開発では、要件定義段階で制約を明文化しておくとAIが逸脱しにくい
- プロパティベーステストは仕様の形式化。「任意の入力に対してこの性質が成り立つ」という記述は、仕様そのもの
- プレイテストからのフィードバックが速い。「ボスが弱い」「安全地帯がある」といった感覚的なフィードバックを、その場でKiroに伝えて調整できる
- テストが多いと方針転換が怖くない。縦シュー→横シューの変更でも、壊れた箇所をすぐ検出できた
- ファミコン制約を先に決めておくと移植が楽になる(はず)。これはPhase 2で検証予定
まとめ
「ファミコンで動くゲームをAIに作ってもらう」という無茶振りから始まったこのプロジェクト、HTML5版は一通り遊べる状態になりました。
Kiro + Claude Opusの組み合わせは、ゲーム開発においても十分に実用的です。特にSpec駆動開発による要件の明文化と、プロパティベーステストによる仕様の形式検証は、ゲームのような「正しさの定義が難しい」ドメインでも有効でした。
次はNES ROM移植。6502との格闘が待っています。
記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。