Tシャツ欲しさに Amazon Q Developer CLI で インベーダーゲームを作ってみた

## はじめに
遅ればせながら 『Amazon Q CLi Developer で ゲームを作ろう』 のキャンペーンに応募すべく、ゲームを作成しました。
Amazon Q  Developer CLI はリリース当初からお世話になっている推しAIツールので、このTシャツは絶対に欲しい。

## 動作環境
- Windows 11
- WSL2
    - ubuntu
- Amazon Q Developer CLI

## セッティング
丁度、Claude4へのモデル変更が可能になったアップデートが入っていたので、せっかくなので最新のClaudeのモデルの性能も確認すべく、モデルの変更を実施。

`/model`でモデル変更の選択肢が現れ、変更が可能に。
もちろん claude4-sonnetを選択。
毎度ですがターミナルに表示されるAAにはロマンがありますね...

![スクリーンショット 2025-06-10 031525.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3236402/4fedd19d-0db5-4fe0-8caf-8f31f36a50fe.png)

`q chat --model claude-4-sonnet`で起動時のモデルをclaude4に変更できるので、マストで変更しておきます。

![スクリーンショット 2025-06-10 031438.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3236402/b4053098-e014-4ad4-a678-272cb71bc65c.png)

## 今回作ったもの
### インベーダーゲーム

ベタですがレトロゲームをサクッと作ってみたかったのでインベーダーゲームにしました。

## 実装
Claude4のパワーもあり、3,4回のやり取りで完成しました。

一発目の出力でほぼほぼインベーダーゲームとしての機能はできあがりました。
- 自機・敵機の存在
- 自弾・敵弾の当たり判定
- 衝突判定からの消滅
- 左右への移動・全身

![スクリーンショット 2025-06-10 033241.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3236402/feb0edb0-6664-4af4-b8ad-06e8b1f07301.png)

しかしながら自機も敵機も完全に豆腐で色気が無さすぎるので、もう少しグラフィカルに、かつゲーム性を持たせるためスコア表示機能の追加を依頼。

![スクリーンショット 2025-06-10 033748.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3236402/afc4bd1b-7d18-40fd-a47a-434d38b710a9.png)


追加で以下の機能も実装しました。
- 残機の追加
- ステージの概念(3面クリアで終了)
- クリアタイムの記録
- GAME OVER 機能
- 敵弾が命中した際の点滅エフェクト
- 自弾が命中した際の爆発エフェクト

開始時は敵機の数が多いのでなかなかの弾幕が降り注ぎます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3236402/92c1541e-bd09-4133-8b5a-4f79dd2a4e34.png)

舐めてると意外と簡単にやられてしまいますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3236402/ece55712-019b-46c9-8ae0-d658be912500.png)

3面倒しきるとクリアです！

![スクリーンショット 2025-06-10 035827.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3236402/1425d3ca-4639-4bc6-bfe3-86ccd4c3c26b.png)

## まとめ
普段はAWSリソースのパラメータ取得やエラー調査ばかり依頼しているAmazon Q Developer CLIですが、Claude4にアップグレードしたこともあり、これくらいのゲームなら余裕で作ってくれますね。

リリース当初から頻繁なアップデートによりどんどん使いやすくなっていくAmazon Q Developer CLI 君に今後も期待しています！！

## 作成してくれたコード
良かったら遊んでみてください！

<details><summary>kuraken_space_invaders.py</summary>

```python
import pygame
import random
import sys

# Initialize Pygame
pygame.init()

# Screen settings
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Space Invaders - Advanced")

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 165, 0)
GRAY = (128, 128, 128)

# Font
font = pygame.font.Font(None, 36)
small_font = pygame.font.Font(None, 24)

# Player class
class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface([50, 30], pygame.SRCALPHA)
        self.original_image = pygame.Surface([50, 30], pygame.SRCALPHA)
        self.draw_fighter()
        self.rect = self.image.get_rect()
        self.rect.x = SCREEN_WIDTH // 2
        self.rect.y = SCREEN_HEIGHT - 50
        self.speed = 5
        self.lives = 3
        self.invincible = False
        self.invincible_timer = 0
        self.blink_timer = 0

    def draw_fighter(self):
        """Draw a fighter jet shape"""
        # Clear both surfaces
        self.image.fill((0, 0, 0, 0))
        self.original_image.fill((0, 0, 0, 0))
        
        # Main body
        pygame.draw.polygon(self.image, GREEN, [
            (25, 0),   # nose
            (15, 20),  # left wing back
            (20, 25),  # left body
            (30, 25),  # right body
            (35, 20),  # right wing back
            (25, 0)    # back to nose
        ])
        # Wings
        pygame.draw.polygon(self.image, GRAY, [
            (10, 15), (15, 20), (20, 25), (15, 25)
        ])
        pygame.draw.polygon(self.image, GRAY, [
            (40, 15), (35, 20), (30, 25), (35, 25)
        ])
        # Cockpit
        pygame.draw.circle(self.image, BLUE, (25, 10), 3)
        
        # Copy to original image
        self.original_image.blit(self.image, (0, 0))

    def take_damage(self):
        """Handle taking damage"""
        if not self.invincible:
            self.lives -= 1
            self.invincible = True
            self.invincible_timer = 120  # 2 seconds at 60 FPS
            return True
        return False

    def update(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT] and self.rect.left > 0:
            self.rect.x -= self.speed
        if keys[pygame.K_RIGHT] and self.rect.right < SCREEN_WIDTH:
            self.rect.x += self.speed
        
        # Handle invincibility and blinking
        if self.invincible:
            self.invincible_timer -= 1
            self.blink_timer += 1
            
            # Blink every 8 frames
            if self.blink_timer % 8 < 4:
                # Make transparent (blink off)
                self.image.fill((0, 0, 0, 0))
            else:
                # Show normal image (blink on)
                self.image.fill((0, 0, 0, 0))
                self.image.blit(self.original_image, (0, 0))
            
            # End invincibility
            if self.invincible_timer <= 0:
                self.invincible = False
                self.blink_timer = 0
                # Restore normal image
                self.image.fill((0, 0, 0, 0))
                self.image.blit(self.original_image, (0, 0))

# Enemy class
class Enemy(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface([30, 30], pygame.SRCALPHA)
        self.draw_ufo()
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.speed_x = 2
        self.speed_y = 20
        self.direction = 1
        self.shoot_timer = random.randint(60, 300)

    def draw_ufo(self):
        """Draw a UFO shape"""
        # Main body (ellipse)
        pygame.draw.ellipse(self.image, GRAY, (5, 10, 20, 10))
        # Top dome
        pygame.draw.ellipse(self.image, RED, (8, 5, 14, 12))
        # Bottom lights
        pygame.draw.circle(self.image, YELLOW, (10, 15), 2)
        pygame.draw.circle(self.image, YELLOW, (15, 15), 2)
        pygame.draw.circle(self.image, YELLOW, (20, 15), 2)

    def update(self):
        # Move horizontally
        self.rect.x += self.speed_x * self.direction
        
        # Check if this enemy hits the screen edge
        if self.rect.right >= SCREEN_WIDTH or self.rect.left <= 0:
            # Move all enemies down and reverse their direction
            for enemy in enemies:
                enemy.rect.y += enemy.speed_y
                enemy.direction *= -1
            # Move back from the edge to prevent getting stuck
            if self.rect.right >= SCREEN_WIDTH:
                self.rect.right = SCREEN_WIDTH
            else:
                self.rect.left = 0
        
        # Shoot randomly
        self.shoot_timer -= 1
        if self.shoot_timer <= 0:
            self.shoot_timer = random.randint(300, 600)
            enemy_bullet = EnemyBullet(self.rect.centerx, self.rect.bottom)
            enemy_bullets.add(enemy_bullet)
            all_sprites.add(enemy_bullet)

# Player bullet class
class Bullet(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface([4, 10])
        self.image.fill(WHITE)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.speed = 7

    def update(self):
        self.rect.y -= self.speed
        if self.rect.bottom < 0:
            self.kill()

# Enemy bullet class
class EnemyBullet(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface([4, 10])
        self.image.fill(RED)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.speed = 3

    def update(self):
        self.rect.y += self.speed
        if self.rect.top > SCREEN_HEIGHT:
            self.kill()

# Explosion effect class
class Explosion(pygame.sprite.Sprite):
    def __init__(self, x, y, score=0):
        super().__init__()
        self.images = []
        # Create explosion animation frames
        for size in [20, 25, 30, 25, 20, 15, 10]:
            img = pygame.Surface([size, size], pygame.SRCALPHA)
            pygame.draw.circle(img, ORANGE, (size//2, size//2), size//2)
            self.images.append(img)
        
        self.index = 0
        self.image = self.images[self.index]
        self.rect = self.image.get_rect()
        self.rect.center = (x, y)
        self.counter = 0
        
        # Score display
        self.score = score
        self.score_counter = 30  # frames to show score
        self.score_y_offset = 0

    def update(self):
        self.counter += 1
        if self.counter >= 4:  # Update animation every 4 frames
            self.counter = 0
            self.index += 1
            if self.index >= len(self.images):
                self.kill()
            else:
                self.image = self.images[self.index]
                
        # Update floating score position
        if self.score > 0:
            self.score_y_offset -= 1
            self.score_counter -= 1
            if self.score_counter <= 0:
                self.score = 0

# Game state
class GameState:
    def __init__(self):
        self.score = 0
        self.level = 1
        self.game_over = False
        self.victory = False
        self.level_clear = False
        self.start_time = pygame.time.get_ticks()
        self.level_start_time = pygame.time.get_ticks()
        self.clear_time = 0

def draw_text(text, x, y, color=WHITE):
    """Draw text on screen"""
    text_surface = font.render(text, True, color)
    screen.blit(text_surface, (x, y))

def game_over_screen():
    """Display game over screen"""
    screen.fill(BLACK)
    draw_text("GAME OVER", SCREEN_WIDTH//2 - 100, SCREEN_HEIGHT//2 - 50, RED)
    draw_text(f"Final Score: {game_state.score}", SCREEN_WIDTH//2 - 120, SCREEN_HEIGHT//2)
    draw_text("Press R to restart or Q to quit", SCREEN_WIDTH//2 - 180, SCREEN_HEIGHT//2 + 50)
    pygame.display.flip()

def victory_screen():
    """Display victory screen"""
    screen.fill(BLACK)
    draw_text("VICTORY!", SCREEN_WIDTH//2 - 80, SCREEN_HEIGHT//2 - 50, GREEN)
    draw_text(f"Final Score: {game_state.score}", SCREEN_WIDTH//2 - 120, SCREEN_HEIGHT//2)
    draw_text("Press R to restart or Q to quit", SCREEN_WIDTH//2 - 180, SCREEN_HEIGHT//2 + 50)
    pygame.display.flip()

def level_clear_screen():
    """Display level clear screen"""
    screen.fill(BLACK)
    draw_text("LEVEL CLEAR!", SCREEN_WIDTH//2 - 100, SCREEN_HEIGHT//2 - 50, GREEN)
    clear_time = (game_state.clear_time - game_state.level_start_time) / 1000  # Convert to seconds
    draw_text(f"Clear Time: {clear_time:.2f} seconds", SCREEN_WIDTH//2 - 150, SCREEN_HEIGHT//2)
    draw_text(f"Score: {game_state.score}", SCREEN_WIDTH//2 - 80, SCREEN_HEIGHT//2 + 50)
    draw_text("Press SPACE to continue", SCREEN_WIDTH//2 - 150, SCREEN_HEIGHT//2 + 100)
    pygame.display.flip()

def create_enemies():
    """Create a new wave of enemies"""
    for row in range(5):
        for column in range(8):
            enemy = Enemy(80 + column * 80, 50 + row * 50)
            enemies.add(enemy)
            all_sprites.add(enemy)
    # Reset level start time when creating new enemies
    game_state.level_start_time = pygame.time.get_ticks()

# Create sprite groups
all_sprites = pygame.sprite.Group()
enemies = pygame.sprite.Group()
bullets = pygame.sprite.Group()
enemy_bullets = pygame.sprite.Group()
explosions = pygame.sprite.Group()

# Create player
player = Player()
all_sprites.add(player)

# Create game state
game_state = GameState()

# Create initial enemies
create_enemies()

# Game loop
running = True
clock = pygame.time.Clock()

while running:
    # Event handling
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if game_state.game_over or game_state.victory:
                if event.key == pygame.K_r:
                    # Restart game
                    game_state = GameState()
                    player.lives = 3
                    player.rect.x = SCREEN_WIDTH // 2
                    player.rect.y = SCREEN_HEIGHT - 50
                    # Clear all sprites except player
                    for sprite in all_sprites:
                        if sprite != player:
                            sprite.kill()
                    enemies.empty()
                    bullets.empty()
                    enemy_bullets.empty()
                    explosions.empty()
                    create_enemies()
                elif event.key == pygame.K_q:
                    running = False
            elif game_state.level_clear:
                if event.key == pygame.K_SPACE:
                    # Continue to next level
                    game_state.level_clear = False
                    game_state.level += 1
                    create_enemies()
                    # Increase difficulty
                    for enemy in enemies:
                        enemy.speed_x += 0.5
            else:
                if event.key == pygame.K_SPACE:
                    bullet = Bullet(player.rect.centerx - 2, player.rect.top)
                    bullets.add(bullet)
                    all_sprites.add(bullet)

    if not game_state.game_over and not game_state.victory and not game_state.level_clear:
        # Update
        all_sprites.update()
        explosions.update()

        # Collision detection - player bullets hit enemies
        hits = pygame.sprite.groupcollide(enemies, bullets, True, True)
        for enemy in hits:
            # Create explosion effect
            explosion = Explosion(enemy.rect.centerx, enemy.rect.centery, 10)
            explosions.add(explosion)
            all_sprites.add(explosion)
            game_state.score += 10

        # Collision detection - enemy bullets hit player
        player_hits = pygame.sprite.spritecollide(player, enemy_bullets, True)
        if player_hits and player.take_damage():
            if player.lives <= 0:
                game_state.game_over = True

        # Check if enemies reach player
        if pygame.sprite.spritecollide(player, enemies, False):
            game_state.game_over = True

        # Check if all enemies are destroyed
        if len(enemies) == 0:
            game_state.clear_time = pygame.time.get_ticks()
            game_state.level_clear = True

        # Check victory condition (for demo purposes, victory at level 3)
        if game_state.level > 3:
            game_state.victory = True

    # Draw
    if game_state.game_over:
        game_over_screen()
    elif game_state.victory:
        victory_screen()
    elif game_state.level_clear:
        level_clear_screen()
    else:
        screen.fill(BLACK)
        all_sprites.draw(screen)
        explosions.draw(screen)
        
        # Draw floating scores from explosions
        for explosion in explosions:
            if explosion.score > 0:
                score_text = small_font.render(f"+{explosion.score}", True, YELLOW)
                screen.blit(score_text, (explosion.rect.centerx - 10, 
                                       explosion.rect.centery - 20 + explosion.score_y_offset))
        
        # Draw UI
        draw_text(f"Score: {game_state.score}", 10, 10)
        draw_text(f"Lives: {player.lives}", 10, 50)
        draw_text(f"Level: {game_state.level}", 10, 90)
        
        # Draw elapsed time
        elapsed_time = (pygame.time.get_ticks() - game_state.level_start_time) / 1000
        draw_text(f"Time: {elapsed_time:.2f}", SCREEN_WIDTH - 150, 10)
        
        pygame.display.flip()

    # Control game speed
    clock.tick(60)

# Quit game
pygame.quit()
sys.exit()

```　
</details>