Q-Developerで、できるだけ雑なinstructionsで倉庫番ゲームを作ってみた！　



## はじめに

みなさんこんにちは！最近、Amazon Q Developer（旧Amazon CodeWhisperer）の進化が素晴らしく、ちょっとしたゲーム開発もできるのではないかと思い、試しに**倉庫番（Sokoban）ゲーム**を作ってみました。

結果として、たった数回のプロンプトで、完全に動作する倉庫番ゲームが完成しました！今回はその開発プロセスと、Q-Developerがどのように私たちのプログラミングを支援してくれるかを紹介したいと思います。
今回、意識したのは「できるだけ雑な指示で、どれくらいのものができるのか」というところを試してみました:laughing:
ですので、プロンプトは非常にやる気ない感じになっています。ご了承ください。

## 今回作ったもの
![1st_iteration.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3641566/c0236816-76a2-4ded-a368-5e02d7787f16.png)
*完成した倉庫番ゲームの画面*

### 実装された機能

- ✅ 基本的な倉庫番のゲームロジック
- ✅ Pygameを使ったGUI
- ✅ 矢印キーでの操作
- ✅ Undo機能（Zキー）
- ✅ リスタート機能（Rキー）
- ✅ ステージクリア判定
- ✅ 自動ステージ生成機能

## 開発環境とツール

- **言語**: Python 3.x
- **ライブラリ**: Pygame
- **AI支援**: Amazon Q Developer
- **開発期間**: 約1時間（ほぼAIとの対話時間、合間に風呂入ったりしながらやってました:relaxed:）

## Q-Developerとの開発プロセス

### ステップ1: 基本要件を伝える

まず、Q-Developerに以下のようなプロンプトを投げました。できるだけ雑な指示を意識しました。

```
Pythonで倉庫番ゲームを作成してください。
- Pygameを使用
- 基本的な倉庫番のルール
- キーボードで操作可能
- シンプルな見た目で良い
```

### ステップ2: 最初の出力を確認

Q-Developerは即座に`sokoban.py`ファイルを生成してくれました！最近のモデルは、
こんな雑なinstructionsでも結構なものを作ってくれるんですね…。
べた書きなのは気になりますが、まあ雑指示ならこんなもんでしょう。

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
倉庫番 (Sokoban) ゲーム
Pythonとpygameを使用したシンプルな倉庫番ゲーム実装
"""

import pygame
import sys
import copy
from maps import get_current_map

# 定数定義
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480
CELL_SIZE = 50
FPS = 60

# 色の定義
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (200, 200, 200)
BLUE = (0, 0, 255)
BROWN = (139, 69, 19)
GREEN = (0, 255, 0)
RED = (255, 0, 0)

# マップ要素の定義
FLOOR = 0
WALL = 1
PLAYER = 2
BOX = 3
GOAL = 4
BOX_ON_GOAL = 5
PLAYER_ON_GOAL = 6

# 現在のステージマップを取得
stage_map = get_current_map()

class Sokoban:
    def __init__(self):
        """ゲームの初期化"""
        pygame.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("倉庫番 (Sokoban)")
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont(None, 48)
        
        # ゲーム状態の初期化
        self.stage = copy.deepcopy(stage_map)
        self.history = []  # Undo用の履歴
        self.game_clear = False
        
        # プレイヤーの位置を取得
        self.player_pos = self.find_player()
    
    def find_player(self):
        """プレイヤーの位置を取得"""
        for y, row in enumerate(self.stage):
            for x, cell in enumerate(row):
                if cell == PLAYER or cell == PLAYER_ON_GOAL:
                    return (x, y)
        return None
    
    def count_remaining_boxes(self):
        """ゴールに乗っていない箱の数を数える"""
        count = 0
        for row in self.stage:
            count += row.count(BOX)
        return count
    
    def draw(self):
        """ゲーム画面の描画"""
        # 背景をグレーで塗りつぶす
        self.screen.fill(GRAY)
        
        # マップの中心位置を計算
        map_width = len(self.stage[0]) * CELL_SIZE
        map_height = len(self.stage) * CELL_SIZE
        offset_x = (SCREEN_WIDTH - map_width) // 2
        offset_y = (SCREEN_HEIGHT - map_height) // 2
        
        # マップの描画
        for y, row in enumerate(self.stage):
            for x, cell in enumerate(row):
                pos_x = offset_x + x * CELL_SIZE
                pos_y = offset_y + y * CELL_SIZE
                
                # セルの種類に応じて描画
                if cell == WALL:
                    # 壁: 黒い四角形
                    pygame.draw.rect(self.screen, BLACK, (pos_x, pos_y, CELL_SIZE, CELL_SIZE))
                elif cell == GOAL or cell == BOX_ON_GOAL or cell == PLAYER_ON_GOAL:
                    # ゴール: 緑の円
                    pygame.draw.circle(self.screen, GREEN, 
                                      (pos_x + CELL_SIZE // 2, pos_y + CELL_SIZE // 2), 
                                      CELL_SIZE // 3)
                
                if cell == PLAYER or cell == PLAYER_ON_GOAL:
                    # プレイヤー: 青い円
                    pygame.draw.circle(self.screen, BLUE, 
                                      (pos_x + CELL_SIZE // 2, pos_y + CELL_SIZE // 2), 
                                      CELL_SIZE // 2 - 5)
                
                if cell == BOX or cell == BOX_ON_GOAL:
                    # 箱: 茶色の四角形
                    pygame.draw.rect(self.screen, BROWN, 
                                    (pos_x + 5, pos_y + 5, CELL_SIZE - 10, CELL_SIZE - 10))
        
        # クリア表示
        if self.game_clear:
            clear_text = self.font.render("STAGE CLEAR!", True, RED)
            text_rect = clear_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
            self.screen.blit(clear_text, text_rect)
    
    def move(self, dx, dy):
        """プレイヤーの移動処理"""
        if self.game_clear:
            return
            
        # 移動先の座標
        new_x, new_y = self.player_pos[0] + dx, self.player_pos[1] + dy
        
        # 移動先が壁の場合は移動できない
        if self.stage[new_y][new_x] == WALL:
            return
            
        # 移動履歴を保存（Undo用）
        self.history.append(copy.deepcopy(self.stage))
        
        # 移動先に箱がある場合
        if self.stage[new_y][new_x] == BOX or self.stage[new_y][new_x] == BOX_ON_GOAL:
            # 箱の移動先の座標
            box_x, box_y = new_x + dx, new_y + dy
            
            # 箱の移動先が壁または別の箱の場合は移動できない
            if (self.stage[box_y][box_x] == WALL or 
                self.stage[box_y][box_x] == BOX or 
                self.stage[box_y][box_x] == BOX_ON_GOAL):
                self.history.pop()  # 履歴から削除
                return
                
            # 箱を移動
            if self.stage[box_y][box_x] == GOAL:
                self.stage[box_y][box_x] = BOX_ON_GOAL
            else:
                self.stage[box_y][box_x] = BOX
        
        # プレイヤーの元の位置を更新
        if self.stage[self.player_pos[1]][self.player_pos[0]] == PLAYER_ON_GOAL:
            self.stage[self.player_pos[1]][self.player_pos[0]] = GOAL
        else:
            self.stage[self.player_pos[1]][self.player_pos[0]] = FLOOR
            
        # プレイヤーの新しい位置を更新
        if self.stage[new_y][new_x] == GOAL or self.stage[new_y][new_x] == BOX_ON_GOAL:
            self.stage[new_y][new_x] = PLAYER_ON_GOAL
        else:
            self.stage[new_y][new_x] = PLAYER
            
        # プレイヤーの位置を更新
        self.player_pos = (new_x, new_y)
        
        # クリア判定
        if self.count_remaining_boxes() == 0:
            self.game_clear = True
    
    def undo(self):
        """一手前の状態に戻す"""
        if self.history:
            self.stage = self.history.pop()
            self.player_pos = self.find_player()
            self.game_clear = False
    
    def restart(self):
        """現在のステージをリスタート"""
        self.stage = copy.deepcopy(get_current_map())
        self.history = []
        self.player_pos = self.find_player()
        self.game_clear = False
    
    def run(self):
        """ゲームのメインループ"""
        running = True
        
        while running:
            # イベント処理
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        running = False
                    elif event.key == pygame.K_UP:
                        self.move(0, -1)
                    elif event.key == pygame.K_DOWN:
                        self.move(0, 1)
                    elif event.key == pygame.K_LEFT:
                        self.move(-1, 0)
                    elif event.key == pygame.K_RIGHT:
                        self.move(1, 0)
                    elif event.key == pygame.K_r:
                        self.restart()
                    elif event.key == pygame.K_z:
                        self.undo()
            
            # 描画
            self.draw()
            
            # 画面更新
            pygame.display.flip()
            self.clock.tick(FPS)
        
        pygame.quit()
        sys.exit()

if __name__ == "__main__":
    game = Sokoban()
    game.run()
```

- 適切なコメントが日本語で書かれている
- ゲームに必要な定数がすべて定義されている(きちんと動くコードが出てる）
- Pygameの基本構造がきちんに実装されている
ってのはやっぱ最近のコーディングエージェントはきっちりやってくるんですね。
1年くらい前だと考えられないですね。


### ステップ3: 機能追加の要望

基本動作を確認した後、以下の機能追加を要求しました。

```
以下の機能を追加してください：
1. Undo機能（一手戻る）
2. リスタート機能
3. ステージクリア時のメッセージ表示
```

Q-Developerはきちんとに対応し、履歴管理とイベントハンドリングを追加してくれました。

```python
def undo(self):
    """一手前の状態に戻す"""
    if self.history:
        self.stage = self.history.pop()
        self.player_pos = self.find_player()
        self.game_clear = False

def restart(self):
    """現在のステージをリスタート"""
    self.stage = copy.deepcopy(get_current_map())
    self.history = []
    self.player_pos = self.find_player()
    self.game_clear = False
```

### ステップ4: ステージ生成機能の追加

![2nd_iteration.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3641566/386b95c7-d3f3-40f4-81a8-935e42fa5201.png)
*ステージ生成機能を追加した後の画面*

さらに欲が出て、自動でステージを生成する機能も要求しました。

```
クリア可能な10x10のステージを自動生成する機能を追加してください
```

Q-Developerは`generate_stage.py`という別ファイルを作成し、以下の高度な機能を実装してくれました。
特にBFSとか意識しなくても、コーディングエージェント側が良しなにやってくれるのは、下手なプログラマじゃ
太刀打ちできないですね…。時代の流れを感じます。

- BFS（幅優先探索）による到達可能性の確認
- 箱の押しやすさの判定
- クリア可能性の検証

```python
def is_reachable(stage, start, end):
    """BFSで2点間の到達可能性を確認"""
    width, height = len(stage[0]), len(stage)
    queue = [start]
    visited = set([start])
    
    while queue:
        x, y = queue.pop(0)
        if (x, y) == end:
            return True
        
        # 上下左右の移動
        for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
            nx, ny = x + dx, y + dy
            if (0 <= nx < width and 0 <= ny < height and 
                stage[ny][nx] != WALL and (nx, ny) not in visited):
                queue.append((nx, ny))
                visited.add((nx, ny))
    
    return False
```

## Q-Developerを使ってみた感想

### 👍 良かった点

1. **コード品質が高い**
   - 適切なコメント
   - わかりやすい変数名
   - Pythonらしい書き方

2. **ゲーム開発の知識が豊富**
   - Pygameの正しい使い方
   - ゲームループの実装
   - イベントハンドリング

3. **段階的な機能追加が得意**
   - 最初はシンプルに、後から複雑な機能を追加
   - 既存コードを壊さない修正

4. **アルゴリズムの実装能力**
   - BFSによる経路探索
   - ゲーム状態の管理

### 🤔 改善が必要だった点

1. **ファイル分割の提案**
   - 最初は全て1ファイルに書かれていた
   - 後でマップデータを別ファイルに分離することを提案

2. **エラーハンドリング**
   - Pygameが未インストールの場合の処理
   - 不正なマップデータの処理

あと、実はここには触れてませんが、マップ生成処理を走らせた際に、生成処理側に不具合があって、カンマが残ってしまっていたのですが、エージェント君にやらせるとループが入りまくって、何度も同じような修正をかまして
正直ちょっとやきもきしました…:kissing:
自分で直しちゃうと意味がないかなーと思って敢えて鬼のようにループさせてみましたが、局所解に入ってしまったのか、似たような修正を何度もやって、「スイカ割りの観客」の気分を味わえました:relaxed:

## 実際に動かしてみよう

### 必要な準備

```bash
# Pygameのインストール
pip install pygame

# ゲームの実行
python sokoban.py
```

### 操作方法

- **矢印キー**: プレイヤーの移動
- **Rキー**: ステージリスタート
- **Zキー**: 一手戻る（Undo）
- **ESCキー**: ゲーム終了

## ソースコード

今回作成したソースコードは以下のような構成になっています：

```
sokoban_game/
├── sokoban.py          # メインゲームファイル
├── maps.py             # ステージマップデータ
├── generate_stage.py   # ステージ自動生成
└── README.md          # 説明書
```

### メインゲームクラスの構造

```python
class Sokoban:
    def __init__(self):
        """ゲームの初期化"""
        pygame.init()
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        # ... 初期化処理
    
    def draw(self):
        """ゲーム画面の描画"""
        # マップ要素の描画ロジック
    
    def move(self, dx, dy):
        """プレイヤーの移動処理"""
        # 移動とコリジョン判定
    
    def run(self):
        """ゲームのメインループ"""
        # イベント処理とゲームループ
```

## カスタマイズのポイント

### 1. 新しいステージの追加

`maps.py`ファイルでステージを追加できます：

```python
stage_maps = {
    "default": [
        [1, 1, 1, 1, 1, 1, 1, 1],
        [1, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 2, 3, 4, 0, 0, 1],
        [1, 0, 0, 1, 0, 4, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1]
    ],
    # 新しいステージを追加
    "your_stage": [
        # あなたのステージデザイン
    ]
}
```

### 2. グラフィックの改善

色やサイズは定数として定義されているので、簡単に変更できます。

```python
# 色の定義を変更
BLUE = (0, 100, 255)    # プレイヤーの色
BROWN = (139, 69, 19)   # 箱の色
GREEN = (0, 200, 0)     # ゴールの色
```

### 3. 難易度の調整

`generate_stage.py`で生成される箱の数やマップサイズを調整

```python
# より難しいステージを生成
stage = generate_solvable_stage(width=12, height=12, num_boxes=5)
```

## まとめ

Q-Developerを使った倉庫番ゲーム開発は想像以上にスムーズでした。特に感動したのは、

1. **ゲーム開発の専門知識**：Pygameの使い方から、ゲームループの実装まで、割としっかりできてた
2. **段階的な機能追加**：基本機能から高度な機能まで、破綻することなく追加
3. **アルゴリズム実装能力**：BFSやゲーム状態管理など、複雑なロジックもよしなに実装
出来ていた点ですね。これくらいできてたら、
「よーし、パパ今からPythonよくわからんけどゲーム作っちゃうぞー」勢の方々も
おおむね満足できるのではないかと。
ゆるふわ日曜プログラマの手遊びに最適だなーと思いました。

これからゲーム開発を始めたい方、特にPythonで手軽に何か作ってみたい方には、Q-Developerとの協働開発はお勧め出来ますね。見た感じ、（今のところは）コストかからなさそうですし。

次回は、このゲームにサウンドや画像リソースを追加して、より本格的なゲームに仕上げてみたいと思います。

## 参考リンク

- [Pygame公式ドキュメント](https://www.pygame.org/docs/)
- [Amazon Q Developer](https://aws.amazon.com/q/developer/)
- [倉庫番のルール解説](https://ja.wikipedia.org/wiki/%E5%80%89%E5%BA%AB%E7%95%AA)

---

この記事が少しでも参考になれば幸いです！質問やコメントがあれば、お気軽にお聞かせください 🎮

#Python #ゲーム開発 #Pygame #AI #プログラミング #倉庫番　#AmazonQCLI