👌

Amazon Q Developerと一緒にAWSテトリスを作ってみた

に公開

Amazon Q Developerと一緒にAWSテトリスを作ってみた

こんにちは!今回は、Amazon Q Developerを使って本格的なテトリスゲームを作成した体験をシェアします。
ただのテトリスではなく、AWS公式アイコンを使用したAWSテーマのテトリスを作りました!

本記事は、「Amazon Q CLI でゲームを作ろう Tシャツキャンペーン」に基づいて作成しています。

https://aws.amazon.com/jp/blogs/news/build-games-with-amazon-q-cli-and-score-a-t-shirt/
https://dev.classmethod.jp/articles/q-developer-cli-game-cbs/

🎯 完成品の紹介

まずは完成したゲームの特徴をご紹介:

  • 7種類のAWSサービステトリミノ(Lambda、S3、EC2、RDS、DynamoDB、CloudFront、API Gateway)
  • AWS公式アイコンを実際に使用
  • 4つのゲームモード(クラシック、タイムアタック、サバイバル、パズル)
  • 特殊ピース(CloudFormation爆弾、X-Ray分析)
  • AWS認定風アチーブメントシステム
  • モダンなUI/UX

ゲーム画面のスクリーンショット

🤖 Amazon Q Developerとの開発体験

開発環境

  • Amazon Q Developer CLI (q chat)
  • HTML/CSS/JavaScript (バニラJS)
  • AWS公式アイコンパッケージ

Q Developerの活用方法

Amazon Q Developerは単なるコード生成ツールでなかく、対話型の開発パートナーとして以下のように活用しました:

1. プロンプト

「AWSをテーマにしたテトリスゲームを作りたいです。HTML/CSS/JavaScriptで開発を行ってください。」
「作成したテトリスゲームをより面白いするうえで必要な機能拡張のアイデアを出してください。」
「出したアイデアに基づいて、段階的に修正してください。修正が完了したら都度READMEに追記しして、ローカルでコミットしてください。」

2. 段階的な機能実装

Q Developerは複雑な機能を段階的に実装することを提案してくれました:

Phase 1: 基本機能

  • ホールド機能
  • ゴーストピース
  • ハードドロップ
  • コンボシステム

Phase 2: 視覚効果

  • 背景テーマ
  • 効果音(Web Audio API)

Phase 3: ゲームモード拡張

  • 複数ゲームモード
  • 特殊ピース
  • アチーブメントシステム

3. 問題解決のサポート

開発中に遭遇した問題も、Q Developerが的確にサポートしてくれました:

問題: キーボード操作時にページがスクロールしてしまう
Q Developerの解決策:

document.addEventListener('keydown', (e) => {
    if (gameRunning && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
        e.preventDefault();
    }
}, { passive: false });

🎨 AWS公式アイコンの実装

アイコンファイルの準備

AWS公式アイコンパッケージから必要なアイコンを抽出:

# 必要なアイコンをコピー
cp "Asset-Package/Architecture-Service-Icons/Arch_Compute/32/Arch_AWS-Lambda_32.png" icons/lambda.png
cp "Asset-Package/Architecture-Service-Icons/Arch_Storage/32/Arch_Amazon-Simple-Storage-Service_32.png" icons/s3.png
# ... 他のサービス
アイコン読み込みシステムの実装コード
// AWS公式アイコンの定義
const AWS_ICONS = {
    'Lambda': 'icons/lambda.png',
    'S3': 'icons/s3.png',
    'EC2': 'icons/ec2.png',
    'RDS': 'icons/rds.png',
    'CloudFront': 'icons/cloudfront.png',
    'DynamoDB': 'icons/dynamodb.png',
    'API Gateway': 'icons/apigateway.png',
    'CloudFormation': 'icons/cloudformation.png',
    'X-Ray': 'icons/xray.png'
};

// アイコン画像のキャッシュ
const iconCache = new Map();

// アイコン画像の読み込み(改良版)
function loadIcon(serviceName) {
    if (iconCache.has(serviceName)) {
        return iconCache.get(serviceName);
    }
    
    const iconPath = AWS_ICONS[serviceName];
    if (!iconPath) {
        console.warn(`No icon path found for service: ${serviceName}`);
        return null;
    }
    
    const img = new Image();
    img.onload = function() {
        console.log(`Successfully loaded icon for ${serviceName}`);
    };
    img.onerror = function() {
        console.warn(`Failed to load icon for ${serviceName}: ${iconPath}`);
    };
    img.src = iconPath;
    iconCache.set(serviceName, img);
    return img;
}

// 全てのアイコンを事前読み込み
function preloadAllIcons() {
    Object.keys(AWS_ICONS).forEach(serviceName => {
        loadIcon(serviceName);
    });
}
AWS公式アイコンの描画処理
// AWS公式アイコンの描画関数
function drawAWSIcon(context, serviceName, x, y, size) {
    const icon = loadIcon(serviceName);
    if (icon && icon.complete && icon.naturalWidth > 0) {
        // アイコンが正常に読み込まれている場合
        const iconSize = Math.floor(size * 0.8);
        const iconX = x + (size - iconSize) / 2;
        const iconY = y + (size - iconSize) / 2;
        
        try {
            context.drawImage(icon, iconX, iconY, iconSize, iconSize);
        } catch (e) {
            console.warn(`Error drawing icon for ${serviceName}:`, e);
            // フォールバック: サービス名を表示
            drawFallbackText(context, serviceName, x, y, size);
        }
    } else {
        // アイコンが読み込まれていない場合のフォールバック
        drawFallbackText(context, serviceName, x, y, size);
    }
}

// フォールバック用テキスト描画
function drawFallbackText(context, serviceName, x, y, size) {
    context.save();
    context.font = `bold ${Math.floor(size * 0.25)}px Arial`;
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.fillStyle = '#ffffff';
    context.strokeStyle = '#000000';
    context.lineWidth = 1;
    
    const centerX = x + size / 2;
    const centerY = y + size / 2;
    const text = serviceName.substring(0, 4);
    
    context.strokeText(text, centerX, centerY);
    context.fillText(text, centerX, centerY);
    context.restore();
}

🎮 ゲーム機能の実装

特殊ピースシステム

AWSの特別なサービスを特殊ピースとして実装:

特殊ピースの定義と効果処理
// 特殊ピース(AWS特別サービス)
const SPECIAL_PIECES = [
    // AWS CloudFormation - インフラ爆弾
    {
        shape: [[1]],
        color: '#ff6b35',
        type: 'bomb',
        special: true,
        awsService: 'CloudFormation',
        icon: '💥'
    },
    // AWS X-Ray - 全体分析
    {
        shape: [[1]],
        color: '#00d4aa',
        type: 'rainbow',
        special: true,
        awsService: 'X-Ray',
        icon: '🔍'
    }
];

// 特殊ピースの生成(AWS対応)
function createSpecialPiece() {
    // 5%の確率で特殊ピース
    if (Math.random() < 0.05) {
        const specialType = SPECIAL_PIECES[Math.floor(Math.random() * SPECIAL_PIECES.length)];
        return {
            ...specialType,
            x: Math.floor(BOARD_WIDTH / 2),
            y: 0
        };
    }
    return null;
}

// 特殊ピースの効果(AWS関連メッセージ付き)
function applySpecialEffect(piece, x, y) {
    if (!piece.special) return;
    
    switch (piece.type) {
        case 'bomb':
            // CloudFormation: インフラ全体を再構築
            for (let dy = -1; dy <= 1; dy++) {
                for (let dx = -1; dx <= 1; dx++) {
                    const newY = y + dy;
                    const newX = x + dx;
                    if (newY >= 0 && newY < BOARD_HEIGHT && newX >= 0 && newX < BOARD_WIDTH) {
                        board[newY][newX] = 0;
                    }
                }
            }
            
            // CloudFormation爆発エフェクト
            const rect = canvas.getBoundingClientRect();
            for (let i = 0; i < 20; i++) {
                setTimeout(() => {
                    createParticle(
                        rect.left + x * BLOCK_SIZE + Math.random() * BLOCK_SIZE * 3,
                        rect.top + y * BLOCK_SIZE + Math.random() * BLOCK_SIZE * 3,
                        '#ff6b35'
                    );
                }, i * 10);
            }
            
            showCombo("CloudFormation Stack Deleted! 💥");
            break;
            
        case 'rainbow':
            // X-Ray: 同じサービスを全て分析・削除
            const targetColor = board[y][x];
            if (targetColor) {
                for (let boardY = 0; boardY < BOARD_HEIGHT; boardY++) {
                    for (let boardX = 0; boardX < BOARD_WIDTH; boardX++) {
                        if (board[boardY][boardX] === targetColor) {
                            board[boardY][boardX] = 0;
                        }
                    }
                }
            }
            
            showCombo("X-Ray Analysis Complete! 🔍");
            break;
    }
}

アチーブメントシステム

AWS認定試験をモチーフにしたアチーブメント:

アチーブメントシステムの実装
// アチーブメント
let achievements = {
    firstClear: false,
    combo10: false,
    hardDrop100: false,
    level10: false
};

// アチーブメント達成(AWS関連メッセージ)
function unlockAchievement(type, message) {
    if (achievements[type]) return; // 既に達成済み
    
    achievements[type] = true;
    saveAchievements();
    updateAchievementDisplay();
    
    // AWS関連メッセージ
    const awsMessages = {
        firstClear: "🎉 AWS Cloud Practitioner レベル達成!",
        combo10: "🔥 Solutions Architect Associate レベル達成!",
        hardDrop100: "⚡ DevOps Engineer Professional レベル達成!",
        level10: "🏆 Solutions Architect Professional レベル達成!"
    };
    
    // ポップアップ表示
    const popup = document.getElementById('achievementPopup');
    popup.textContent = awsMessages[type] || `🏆 ${message}`;
    popup.classList.add('show');
    
    setTimeout(() => {
        popup.classList.remove('show');
    }, 3000);
    
    // 達成音
    sounds.levelUp();
}

// アチーブメントの保存
function saveAchievements() {
    localStorage.setItem('tetrisAchievements', JSON.stringify(achievements));
}

// アチーブメント表示の更新
function updateAchievementDisplay() {
    document.getElementById('achievement1').style.color = achievements.firstClear ? '#ffd700' : '#666';
    document.getElementById('achievement2').style.color = achievements.combo10 ? '#ffd700' : '#666';
    document.getElementById('achievement3').style.color = achievements.hardDrop100 ? '#ffd700' : '#666';
    document.getElementById('achievement4').style.color = achievements.level10 ? '#ffd700' : '#666';
}

🔧 技術的な工夫

1. パフォーマンス最適化

  • アイコンキャッシュ: 重複読み込みを防止
  • 事前読み込み: ゲーム開始時に全アイコンを読み込み
  • 効率的な描画: requestAnimationFrameを使用
ゲームループとパフォーマンス最適化
// ゲームループ(修正版)
function gameLoop(timestamp) {
    if (gameRunning) {
        // 初回実行時のdropTime初期化
        if (dropTime === 0) {
            dropTime = timestamp;
        }
        
        if (timestamp - dropTime > dropInterval) {
            if (currentPiece && !isCollision(currentPiece, 0, 1)) {
                currentPiece.y++;
            } else if (currentPiece) {
                lockPiece();
            }
            dropTime = timestamp;
        }
        
        drawBoard();
        drawNextPiece();
        drawHoldPiece();
    }
    
    requestAnimationFrame(gameLoop);
}

// ゲームの初期化と開始(修正版)
function initGame() {
    try {
        initAudioContext(); // オーディオコンテキストの初期化
        loadHighScore(); // ハイスコアの読み込み
        loadAchievements(); // アチーブメントの読み込み
        
        // AWS公式アイコンの事前読み込み
        console.log('Loading AWS service icons...');
        preloadAllIcons();
        
        initBoard();
        currentPiece = createPiece();
        nextPiece = createPiece();
        holdPiece = null;
        canHold = true;
        combo = 0;
        dropTime = 0; // dropTimeを初期化
        updateDisplay();
        drawHoldPiece();
        requestAnimationFrame(gameLoop);
        console.log('Game initialized with AWS icons');
    } catch (error) {
        console.error('Game initialization error:', error);
        // 基本的な初期化のみ実行
        initBoard();
        currentPiece = createPiece();
        nextPiece = createPiece();
        dropTime = 0;
        updateDisplay();
        requestAnimationFrame(gameLoop);
    }
}

2. エラーハンドリング

堅牢なエラーハンドリング実装
// エラーハンドリング強化版のピース描画
function drawPiece(piece, offsetX = 0, offsetY = 0, context = ctx, alpha = 1.0) {
    context.globalAlpha = alpha;
    
    for (let y = 0; y < piece.shape.length; y++) {
        for (let x = 0; x < piece.shape[y].length; x++) {
            if (piece.shape[y][x]) {
                const blockX = (piece.x + x + offsetX) * BLOCK_SIZE;
                const blockY = (piece.y + y + offsetY) * BLOCK_SIZE;
                
                // ブロックの描画(グラデーション効果)
                const gradient = context.createLinearGradient(
                    blockX, blockY, 
                    blockX + BLOCK_SIZE, blockY + BLOCK_SIZE
                );
                gradient.addColorStop(0, piece.color);
                gradient.addColorStop(1, darkenColor(piece.color, 0.3));
                
                context.fillStyle = gradient;
                context.fillRect(blockX, blockY, BLOCK_SIZE - 1, BLOCK_SIZE - 1);
                
                // ハイライト効果
                context.fillStyle = lightenColor(piece.color, 0.3);
                context.fillRect(blockX, blockY, BLOCK_SIZE - 1, 3);
                context.fillRect(blockX, blockY, 3, BLOCK_SIZE - 1);
                
                // AWS公式アイコンを描画
                if (piece.awsService && BLOCK_SIZE >= 20) {
                    drawAWSIcon(context, piece.awsService, blockX, blockY, BLOCK_SIZE);
                }
            }
        }
    }
    context.globalAlpha = 1.0;
}

// キーボード操作の最適化(スクロール防止)
document.addEventListener('keydown', (e) => {
    if (!gameRunning || !currentPiece) return;
    
    // オーディオコンテキストの再開(ユーザーインタラクション後)
    if (audioContext && audioContext.state === 'suspended') {
        audioContext.resume();
    }
    
    switch (e.key) {
        case 'ArrowLeft':
            e.preventDefault(); // 左矢印キーのデフォルト動作を防ぐ
            if (!isCollision(currentPiece, -1, 0)) {
                currentPiece.x--;
                sounds.move();
            }
            break;
        case 'ArrowRight':
            e.preventDefault(); // 右矢印キーのデフォルト動作を防ぐ
            if (!isCollision(currentPiece, 1, 0)) {
                currentPiece.x++;
                sounds.move();
            }
            break;
        case 'ArrowDown':
            e.preventDefault(); // 下矢印キーのデフォルト動作を防ぐ(スクロール防止)
            if (!isCollision(currentPiece, 0, 1)) {
                currentPiece.y++;
                score += 1;
                updateDisplay();
                sounds.move();
            }
            break;
        case 'ArrowUp':
            e.preventDefault(); // 上矢印キーのデフォルト動作を防ぐ
            const rotated = rotatePiece(currentPiece);
            if (!isCollision(rotated)) {
                currentPiece = rotated;
                sounds.rotate();
            }
            break;
        case ' ':
            e.preventDefault(); // スペースキーのデフォルト動作を防ぐ
            hardDrop();
            break;
        case 'c':
        case 'C':
            e.preventDefault(); // Cキーのデフォルト動作を防ぐ
            holdCurrentPiece();
            break;
    }
});

// ページ全体でのキーボードスクロールを防ぐ(ゲーム中のみ)
document.addEventListener('keydown', (e) => {
    // ゲーム中の場合、矢印キーとスペースキーのデフォルト動作を防ぐ
    if (gameRunning && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
        e.preventDefault();
    }
}, { passive: false });

3. レスポンシブ対応

CSS レスポンシブデザイン
body {
    margin: 0;
    padding: 20px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    font-family: 'Arial', sans-serif;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    transition: background 2s ease;
}

.game-container {
    display: flex;
    gap: 20px;
    background: rgba(255, 255, 255, 0.1);
    padding: 20px;
    border-radius: 15px;
    backdrop-filter: blur(10px);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}

.settings-panel {
    position: fixed;
    top: 20px;
    right: 20px;
    background: rgba(255, 255, 255, 0.1);
    padding: 15px;
    border-radius: 10px;
    backdrop-filter: blur(10px);
    color: white;
    z-index: 50;
}

@media (max-width: 768px) {
    .game-container {
        flex-direction: column;
        align-items: center;
    }
    
    .settings-panel {
        position: relative;
        top: auto;
        right: auto;
        margin-bottom: 20px;
    }
}

📊 開発プロセスと学び

段階的開発のメリット

Q Developerの提案に従って段階的に開発したことで:

  1. 早期のフィードバック: 各段階で動作確認
  2. 問題の早期発見: 小さな単位での問題解決
  3. モチベーション維持: 段階的な達成感

Q Developerとの協働で学んだこと

良かった点

  • コード品質の向上: ベストプラクティスの提案
  • エラーハンドリング: 堅牢なコードの書き方
  • ドキュメント作成: 詳細なREADMEの自動生成

改善点

  • 複雑な要件: 非常に複雑な要件は段階的に伝える必要
  • デザイン面: UIデザインは人間の感性が重要

🎯 完成したゲームの特徴

ゲームモード

  1. クラシックモード: 従来のテトリス
  2. タイムアタックモード: 120秒でハイスコア競争
  3. サバイバルモード: 初期ブロック配置ありの高難易度
  4. パズルモード: 特定課題クリア型
ゲームモード実装コード
// ゲームモードの設定
function setGameMode(mode) {
    gameMode = mode;
    
    // ボタンのアクティブ状態を更新
    document.querySelectorAll('.game-mode-selector button').forEach(btn => {
        btn.classList.remove('active');
    });
    document.getElementById(mode + 'Mode').classList.add('active');
    
    // モード固有の設定
    switch (mode) {
        case 'timeAttack':
            timeLeft = 120;
            document.getElementById('timerDisplay').style.display = 'block';
            break;
        case 'survival':
            // サバイバルモード用の初期設定
            break;
        case 'puzzle':
            // パズルモード用の初期設定
            break;
        default:
            document.getElementById('timerDisplay').style.display = 'none';
    }
    
    restartGame();
}

// タイマー更新
function updateTimer() {
    if (gameMode === 'timeAttack' && gameRunning) {
        timeLeft--;
        document.getElementById('timer').textContent = timeLeft;
        
        if (timeLeft <= 0) {
            gameOver();
        }
    }
}

// ゲームの再開(モード対応・修正版)
function restartGame() {
    // タイマーをクリア
    if (timerInterval) {
        clearInterval(timerInterval);
        timerInterval = null;
    }
    
    initBoard();
    
    // サバイバルモードの場合、初期ブロックを配置
    if (gameMode === 'survival') {
        for (let y = BOARD_HEIGHT - 5; y < BOARD_HEIGHT; y++) {
            for (let x = 0; x < BOARD_WIDTH; x++) {
                if (Math.random() < 0.7) {
                    const randomPiece = PIECES[Math.floor(Math.random() * PIECES.length)];
                    board[y][x] = {
                        color: randomPiece.color,
                        awsService: randomPiece.awsService
                    };
                }
            }
        }
    }
    
    currentPiece = createPiece();
    nextPiece = createPiece();
    holdPiece = null;
    canHold = true;
    score = 0;
    level = 1;
    lines = 0;
    combo = 0;
    dropInterval = 1000;
    dropTime = 0;
    gameRunning = true;
    gamesPlayed++;
    
    // タイムアタックモードの場合、タイマーを開始
    if (gameMode === 'timeAttack') {
        timeLeft = 120;
        timerInterval = setInterval(updateTimer, 1000);
    }
    
    document.getElementById('gameOver').style.display = 'none';
    updateDisplay();
    drawHoldPiece();
}

AWS要素の統合

  • テトリミノ: 各ピースがAWSサービスを表現
  • 特殊効果: CloudFormationの爆発、X-Rayの分析
  • アチーブメント: AWS認定試験をモチーフ
  • サービス情報: 現在のピースのAWSサービス説明を表示
AWSサービス情報表示システム
// AWSサービス情報の定義
const AWS_SERVICE_INFO = {
    'Lambda': {
        name: 'AWS Lambda',
        description: 'サーバーレスコンピューティングサービス。コードを実行するためのサーバーの管理が不要です。',
        icon: '🔧'
    },
    'S3': {
        name: 'Amazon S3',
        description: 'スケーラブルなオブジェクトストレージサービス。データの保存と取得が可能です。',
        icon: '🪣'
    },
    'EC2': {
        name: 'Amazon EC2',
        description: 'クラウド上の仮想サーバー。スケーラブルなコンピューティング容量を提供します。',
        icon: '🖥️'
    },
    'RDS': {
        name: 'Amazon RDS',
        description: 'マネージドリレーショナルデータベースサービス。データベースの管理を簡素化します。',
        icon: '🗄️'
    },
    'CloudFront': {
        name: 'Amazon CloudFront',
        description: 'グローバルコンテンツ配信ネットワーク(CDN)。高速なコンテンツ配信を実現します。',
        icon: '🌐'
    },
    'DynamoDB': {
        name: 'Amazon DynamoDB',
        description: 'NoSQLデータベースサービス。高速で予測可能なパフォーマンスを提供します。',
        icon: '⚡'
    },
    'API Gateway': {
        name: 'Amazon API Gateway',
        description: 'APIの作成、公開、管理を行うサービス。RESTful APIとWebSocket APIをサポートします。',
        icon: '🚪'
    },
    'CloudFormation': {
        name: 'AWS CloudFormation',
        description: 'インフラストラクチャをコードとして管理するサービス。リソースの作成と管理を自動化します。',
        icon: '💥'
    },
    'X-Ray': {
        name: 'AWS X-Ray',
        description: 'アプリケーションの分析とデバッグサービス。分散アプリケーションの動作を可視化します。',
        icon: '🔍'
    }
};

// 現在のAWSサービス情報を更新
function updateAWSServiceInfo(piece) {
    if (piece && piece.awsService) {
        const serviceInfo = AWS_SERVICE_INFO[piece.awsService];
        if (serviceInfo) {
            document.getElementById('currentServiceName').textContent = 
                `${serviceInfo.icon} ${serviceInfo.name}`;
            document.getElementById('currentServiceDescription').textContent = 
                serviceInfo.description;
        }
    }
}

📈 パフォーマンスと最適化

最終的な構成

game/
├── tetris.html          # メインゲーム(約65KB)
├── icons/              # AWS公式アイコン(9ファイル)
├── tetris_simple.html   # テスト用シンプル版
└── README.md           # 詳細ドキュメント

読み込み時間

  • 初期読み込み: 約200ms
  • アイコン読み込み: 約100ms
  • ゲーム開始: 約50ms

💡 まとめ

Amazon Q Developerを使うことでアプリ開発経験のない私でも簡単にゲームを作成して、機能拡張まで実装できました。

Q Developerの価値

  1. 設計相談パートナー: アーキテクチャや実装方針の相談
  2. コード品質向上: ベストプラクティスの提案
  3. 問題解決支援: エラーやバグの迅速な解決
  4. ドキュメント作成: 包括的なドキュメント生成
  5. アイデア提案: 機能拡張アイデアの案出し

開発者としての学び

  • 段階的開発の重要性: 複雑な機能も小さく分けて実装
  • エラーハンドリング: 堅牢なアプリケーション設計
  • ユーザー体験: 技術だけでなくUXも重要

🔗 リソース

Amazon Q Developerは、開発を支援する強力なツールです。皆さんもぜひ、Q Developerと一緒に面白いプロジェクトを作ってみてください!

Discussion