皆さんこんにちは!
学校の夏休みの宿題より塾の課題のほうが多かった小学生時代でした。
どうもいとさんです。
先日アマゾン ウェブサービス ジャパン合同会社主催の『はじめてのAI駆動開発 on AWS』に参加した際に、KiroとAmazon Q CLIを使用し簡単なゲームを作成したのでレポートとともに完成までの時間や特徴を比較していこうと思います!
また本記事の作成にあたりNTT東日本株式会社の白鳥さんにご協力いただきました。
お忙しい所ご協力いただき誠にありがとうございました。
イベントレポート
今回はPartner限定ということで詳細はお話しできませんがこのような形でイベントが進んでいきました。
- Amazon Q CLIについて・ユースケース
- Kiroの詳細・ユースケース
- ハンズオン:Amazon Q CLIでS3へのアップロードまで全部行おう(手作業での修正禁止✖)
- 成果物の発表
新しいKiroについては詳細を知らなかったのでとても有益な情報でした。
ユースケースに関してはAWSで環境構築している人向けの内容だと思いました。(トラブルシューティングなど)
レベルが100~200向けということもあり初学者の私でも理解しやすい内容でした。
Amazon Q CLIとKiroの比較
今回ハンズオンではAmazon Q CLIの使用が主な目的でしたが、せっかくなのでKiroとの違いを見てみたいと思い
今回の機会を利用しプロンプトを投げてから完成までを比較してみました。
使用したプロンプト
差が出ないように以下の共通のプロンプトを使用いたしました。
こちらもKiroに作成してもらいました。
AWSサービスアイコンを使用したマッチ3風パズルゲームを開発します。プレイヤーは縦12マス×横6マスのフィールドでAWSサービスアイコンをクリックし、4つ以上縦横につながったアイコンを消してスコアを競います。難易度に応じてアイコンの種類が増え、1分間の制限時間内でできるだけ多くのアイコンを消すことが目標です。 Requirements Requirement 1 User Story: ゲームプレイヤーとして、AWSアイコンを使ったパズルゲームをプレイしたいので、直感的で楽しいゲーム体験を得たい Acceptance Criteria WHEN ゲームが開始されるとき THEN システムは縦12マス×横6マスのゲームフィールドを表示する WHEN ゲームが開始されるとき THEN システムは選択された難易度に応じたAWSアイコンをランダムに配置する WHEN プレイヤーがアイコンをクリックするとき THEN システムは同じ種類のアイコンが4つ以上縦横につながっているかを判定する WHEN 4つ以上のアイコンがつながっているとき THEN システムはそれらのアイコンを消去し、スコアに加算する WHEN アイコンが消去されるとき THEN システムは上から新しいアイコンを降らせて空いたスペースを埋める Requirement 2 User Story: ゲームプレイヤーとして、自分のスキルレベルに合った難易度でプレイしたいので、適切な挑戦レベルを選択できるようにしたい Acceptance Criteria WHEN ゲーム開始前に難易度を選択するとき THEN システムは「簡単」「普通」「難しい」「神」の4つの選択肢を提供する WHEN 「簡単」が選択されるとき THEN システムはEC2、VPC、S3、RDSの4種類のアイコンを使用する WHEN 「普通」が選択されるとき THEN システムは簡単レベルに加えてLambda、DynamoDB、API Gateway、EventBridgeを含む8種類のアイコンを使用する WHEN 「難しい」が選択されるとき THEN システムは普通レベルに加えてBedrock、SageMaker、Polly、Transcribeを含む12種類のアイコンを使用する WHEN 「神」が選択されるとき THEN システムは100種類のAWSサービスアイコンを使用する Requirement 3 User Story: ゲームプレイヤーとして、制限時間内でのスコアを競いたいので、明確な時間制限とスコア表示が欲しい Acceptance Criteria WHEN ゲームが開始されるとき THEN システムは1分間のカウントダウンタイマーを開始する WHEN タイマーが動作中のとき THEN システムは残り時間を秒単位で表示する WHEN アイコンが消去されるとき THEN システムは消去されたアイコン数をスコアに加算し、リアルタイムで表示する WHEN 制限時間が終了するとき THEN システムはゲームを終了し、最終スコアを表示する WHEN ゲーム終了時 THEN システムは新しいゲームを開始するオプションを提供する Requirement 4 User Story: ゲームプレイヤーとして、本物のAWSアイコンを使った視覚的に魅力的なゲームをプレイしたいので、公式のAWSアーキテクチャアイコンが使用されるべきだ Acceptance Criteria WHEN ゲームでアイコンが表示されるとき THEN システムはAWS公式アーキテクチャアイコン(https://aws.amazon.com/jp/architecture/icons/)を使用する WHEN アイコンが表示されるとき THEN システムは各アイコンを明確に識別できるサイズと品質で表示する WHEN アイコンが選択されるとき THEN システムは視覚的なフィードバック(ハイライトなど)を提供する WHEN アイコンが消去されるとき THEN システムはスムーズなアニメーション効果を表示する Requirement 5 User Story: ゲーム管理者として、シンプルなデプロイメントと配布を実現したいので、単一のHTMLファイルとしてゲームが動作する必要がある Acceptance Criteria WHEN ゲームが開発されるとき THEN システムは単一のHTMLファイルとして実装される WHEN HTMLファイルが作成されるとき THEN システムは必要なJavaScriptとCSSをインライン化する WHEN ゲームがデプロイされるとき THEN システムはS3バケットの静的Webサイトホスティングで動作する WHEN ゲームがロードされるとき THEN システムは外部依存関係なしに完全に動作する WHEN ブラウザでHTMLファイルが開かれるとき THEN システムは即座にゲームを開始できる状態になる
完成目標は以下5点です
- レベル選択ボタン、開始ボタンがあること
- 3つ以上でブロックが消えること
- スコアが記入されること
- 単一のHTMLファイルで構成されていること
- ブロックは公式アイコンを使用すること
Amazon Q CLI
まずはAmazon Q CLIに作ってもらいました。
Ver1
何も表示されないしゲームが始まりもしません…
修正しましょう
修正点
- ブロックが表示されないので原因を調査してください(2回投げました)
- 公式アイコンのリンクを認識しなかったのでローカルからコピーしてください
- スタート画面がありません。作成してください
- ゲーム中に難易度が移動できないようにスタート画面に選択ボタンを移動してください。
- ブロックが消える際に消したサービスの文字列を表示してください。消え方はフェードアウトで
完成版
全難易度のプレイ画面(特に何も変わらない…)
カーソルをブロックに持っていくとサービス名が出ました(うれしい)
完成までは約37分
作成されたファイルはHTMLファイルのみで、使用したファイルは引用するためのアイコン画像ファイルです。
・aws-puzzle-game.html
・─ icons/
├── ec2.png
├── s3.png
├── rds.png
├── vpc.png
└── lambda.png
Kiro
次はKiroです。
要件定義書➡詳細設計タスク作成約5分
このような感じでタスクの実行が進んでいきます。(チームメンバーの方のKiroで作成したため以下画面は別作成の物です)
プロンプトからver1完成まで約1時間半かかり、task数は14になりました。
ver1はアイコンがサイトからの指定だと反映されなかったため絵文字になってしましました…
ということで以下の点を修正いたしました。
修正点
- アイコンが公式HPのファイルから反映できなかったのでローカルから読み込み、ブロックの配置を難易度ごとに変更
- ブロックの消える際のエフェクト追加
- カーソルの判定がおかしいので修正
完成版
簡単レベル
普通レベル
「難しい」レベル
「HERO」レベル
Kiro版は完成まで約4時間半かかり
作成したファイルはHTMLファイルのみです。
・aws-Icon-puzzle.html
使用したファイルは引用するためのアイコン画像ファイルです。
・公式アイコンファイル(ブロックアイコンのため使用)
結果
完成目標はAmazon Q CLIはHEROレベルを考慮していないので作成時間に大きく時間に差が出ており、ver1までの時間はKiroのほうがかかりました。
Kiroの時間がかかった要因はファイルの検索、読み込み、デバッグや結合、単体テストを行っていた時間を含んでいたためであり、途中でゲームの動作確認は行えませんでした。
Kiroは時間はかかりましたがver1で表示されない、動かないなどのバグはなく完成版までの修正は少なかったです。
Amazon Q CLIは大枠のUIやシステムの作成は早いのですがデバッグなどは行わずコーディングしていくので、表示されない、選択できないなどバグが多く出やすかったです。
htmlファイル1つで作成しアクションを追加していたためKiro版は10000行を超えてしまいました。
これだけ大きくなってしまうと修正が難しくなっていったようです。(白鳥さん談)
特徴・まとめ
Amazon Q CLIでのコーディングは複雑な指示を投げるより大雑把な指示を投げてから細かくプロンプトで指示を行うと手戻りが少なくなるのかなと思いました。
使用しているAWS環境での問題解決やWEBデザイン作成が向いていると感じました。(複雑なものは厳しいのかな)
Kiroは時間がかかるが手戻りが少なく、大きなバグが少ない印象です。
AI仕様駆動開発の名の通り仕様書からテストまで一貫して行い、修正少なく楽に作成できました。
どちらも使用用途によってメリット・デメリットがありますが共通してコーディング・トラブルシュートが楽だなと感じました。
コンソールでも使用できるとのことだったのでコーディング以外でも使いこなしていきたいと思います!
おまけ
今回作成したAmazon Q CLIのゲームのみですが以下に記載させていただきます。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AWS パズルゲーム</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #232F3E, #FF9900); color: white; text-align: center; } .game-container { max-width: 800px; margin: 0 auto; } .header { margin-bottom: 20px; } .stats { display: flex; justify-content: space-around; margin-bottom: 20px; font-size: 18px; font-weight: bold; } .game-board { display: grid; grid-template-columns: repeat(6, 50px); grid-template-rows: repeat(12, 50px); gap: 2px; justify-content: center; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 10px; margin: 0 auto; } .cell { width: 50px; height: 50px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.3s ease; position: relative; } .cell:hover { background: rgba(255,255,255,0.2); transform: scale(1.05); } .cell.selected { background: rgba(255,153,0,0.5); border-color: #FF9900; } .cell.matched { animation: fadeOut 1s ease-in-out forwards; } @keyframes fadeOut { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.1); } 100% { opacity: 0; transform: scale(0.8); } } .service-name { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: white; padding: 4px 8px; border-radius: 4px; font-size: 10px; white-space: nowrap; opacity: 0; animation: fadeInOut 1s ease-in-out; z-index: 10; } @keyframes fadeInOut { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0; } } .icon { width: 40px; height: 40px; background-size: contain; background-repeat: no-repeat; background-position: center; } .start-btn { background: #FF9900; color: white; border: none; padding: 15px 30px; margin: 20px; border-radius: 5px; cursor: pointer; font-size: 18px; font-weight: bold; } .start-btn:hover { background: #e68900; } .start-btn:disabled { background: #666; cursor: not-allowed; } .start-screen { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 400px; } .game-screen { display: none; } .game-screen.active { display: block; } .difficulty-selector { margin-bottom: 20px; } .difficulty-btn { background: #FF9900; color: white; border: none; padding: 10px 20px; margin: 5px; border-radius: 5px; cursor: pointer; font-size: 16px; } .difficulty-btn:hover { background: #e68900; } .difficulty-btn.selected { background: #232F3E; } .game-over { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); display: none; align-items: center; justify-content: center; z-index: 1000; } .game-over-content { background: #232F3E; padding: 40px; border-radius: 10px; text-align: center; } .restart-btn { background: #FF9900; color: white; border: none; padding: 15px 30px; margin-top: 20px; border-radius: 5px; cursor: pointer; font-size: 18px; } </style> </head> <body> <div class="game-container"> <h1>AWS パズルゲーム</h1> <div class="start-screen" id="startScreen"> <h2>難易度を選択してください</h2> <div class="difficulty-selector"> <button class="difficulty-btn" data-level="easy">簡単 (4種類)</button> <button class="difficulty-btn" data-level="normal">普通 (8種類)</button> <button class="difficulty-btn" data-level="hard">難しい (12種類)</button> <button class="difficulty-btn" data-level="god">神 (100種類)</button> </div> <button class="start-btn" id="startBtn" onclick="startNewGame()">ゲーム開始</button> </div> <div class="game-screen" id="gameScreen"> <div class="stats"> <div>難易度: <span id="currentDifficulty">簡単</span></div> <div>スコア: <span id="score">0</span></div> <div>残り時間: <span id="timer">60</span>秒</div> </div> <div class="game-board" id="gameBoard"></div> <button class="restart-btn" onclick="backToStart()">スタート画面に戻る</button> </div> </div> <div class="game-over" id="gameOver"> <div class="game-over-content"> <h2>ゲーム終了!</h2> <p>最終スコア: <span id="finalScore">0</span></p> <button class="restart-btn" onclick="startNewGame()">新しいゲーム</button> </div> </div> <script> class AWSPuzzleGame { constructor() { this.board = []; this.score = 0; this.timeLeft = 60; this.gameActive = false; this.selectedCell = null; this.difficulty = 'easy'; this.timer = null; // AWS サービスアイコンのデータ this.awsIcons = { 'EC2': './icons/ec2.png', 'VPC': './icons/vpc.png', 'S3': './icons/s3.png', 'RDS': './icons/rds.png', 'Lambda': './icons/lambda.png', 'DynamoDB': './icons/ec2.png', 'API Gateway': './icons/s3.png', 'EventBridge': './icons/vpc.png', 'Bedrock': './icons/rds.png', 'SageMaker': './icons/lambda.png', 'Polly': './icons/ec2.png', 'Transcribe': './icons/s3.png' }; console.log('AWS Icons loaded:', this.awsIcons); // 神レベル用の追加アイコン(100種類まで拡張) this.godIcons = {}; for (let i = 1; i <= 100; i++) { this.godIcons[`Service${i}`] = this.getRandomColor(); } this.difficultySettings = { easy: Object.keys(this.awsIcons).slice(0, 4), normal: Object.keys(this.awsIcons).slice(0, 8), hard: Object.keys(this.awsIcons), god: Object.keys(this.godIcons) }; this.initializeGame(); } getRandomColor() { const colors = ['#FF9900', '#232F3E', '#569A31', '#3F48CC', '#FF4F8B', '#8C4FFF', '#FF6B6B', '#4ECDC4']; return colors[Math.floor(Math.random() * colors.length)]; } initializeGame() { this.createBoard(); this.setupEventListeners(); } createBoard() { const gameBoard = document.getElementById('gameBoard'); gameBoard.innerHTML = ''; this.board = []; for (let row = 0; row < 12; row++) { this.board[row] = []; for (let col = 0; col < 6; col++) { const cell = document.createElement('div'); cell.className = 'cell'; cell.dataset.row = row; cell.dataset.col = col; const icon = document.createElement('div'); icon.className = 'icon'; cell.appendChild(icon); gameBoard.appendChild(cell); this.board[row][col] = this.getRandomIcon(); this.updateCellDisplay(cell, this.board[row][col]); } } } getRandomIcon() { const icons = this.difficultySettings[this.difficulty]; return icons[Math.floor(Math.random() * icons.length)]; } updateCellDisplay(cell, iconType) { const icon = cell.querySelector('.icon'); if (this.difficulty === 'god') { const color = this.godIcons[iconType]; icon.style.background = color; icon.style.backgroundImage = 'none'; icon.style.borderRadius = '50%'; } else { const imagePath = this.awsIcons[iconType]; icon.style.backgroundImage = `url('${imagePath}')`; icon.style.backgroundColor = '#FF9900'; icon.style.borderRadius = '5px'; } icon.title = iconType; console.log('Icon updated:', iconType, this.awsIcons[iconType]); } setupEventListeners() { document.querySelectorAll('.difficulty-btn').forEach(btn => { btn.addEventListener('click', (e) => { this.setDifficulty(e.target.dataset.level); }); }); document.getElementById('gameBoard').addEventListener('click', (e) => { if (!this.gameActive) return; const cell = e.target.closest('.cell'); if (cell) { this.handleCellClick(cell); } }); } setDifficulty(level) { if (this.gameActive) return; // ゲーム中は難易度変更不可 this.difficulty = level; document.querySelectorAll('.difficulty-btn').forEach(btn => { btn.classList.remove('selected'); }); document.querySelector(`[data-level="${level}"]`).classList.add('selected'); // 難易度名を更新 const difficultyNames = { easy: '簡単', normal: '普通', hard: '難しい', god: '神' }; document.getElementById('currentDifficulty').textContent = difficultyNames[level]; } handleCellClick(cell) { const row = parseInt(cell.dataset.row); const col = parseInt(cell.dataset.col); const iconType = this.board[row][col]; if (this.selectedCell) { this.selectedCell.classList.remove('selected'); } this.selectedCell = cell; cell.classList.add('selected'); const matches = this.findMatches(row, col); if (matches.length >= 3) { this.removeMatches(matches, iconType); this.score += matches.length; document.getElementById('score').textContent = this.score; setTimeout(() => this.dropIcons(), 1000); } setTimeout(() => { if (this.selectedCell) { this.selectedCell.classList.remove('selected'); this.selectedCell = null; } }, 500); } findMatches(row, col) { const iconType = this.board[row][col]; const visited = new Set(); const matches = []; const dfs = (r, c) => { const key = `${r},${c}`; if (visited.has(key) || r < 0 || r >= 12 || c < 0 || c >= 6 || this.board[r][c] !== iconType) { return; } visited.add(key); matches.push({row: r, col: c}); // 縦横の隣接セルをチェック dfs(r-1, c); // 上 dfs(r+1, c); // 下 dfs(r, c-1); // 左 dfs(r, c+1); // 右 }; dfs(row, col); return matches; } removeMatches(matches, serviceName) { matches.forEach(match => { const cell = document.querySelector(`[data-row="${match.row}"][data-col="${match.col}"]`); cell.classList.add('matched'); // サービス名を表示 const nameDiv = document.createElement('div'); nameDiv.className = 'service-name'; nameDiv.textContent = serviceName; cell.appendChild(nameDiv); this.board[match.row][match.col] = null; }); setTimeout(() => { matches.forEach(match => { const cell = document.querySelector(`[data-row="${match.row}"][data-col="${match.col}"]`); cell.classList.remove('matched'); const nameDiv = cell.querySelector('.service-name'); if (nameDiv) nameDiv.remove(); }); }, 1000); } dropIcons() { for (let col = 0; col < 6; col++) { let writePos = 11; // 下から上へスキャンして、nullでないアイコンを下に詰める for (let row = 11; row >= 0; row--) { if (this.board[row][col] !== null) { this.board[writePos][col] = this.board[row][col]; if (writePos !== row) { this.board[row][col] = null; } writePos--; } } // 上の空いた部分に新しいアイコンを追加 for (let row = writePos; row >= 0; row--) { this.board[row][col] = this.getRandomIcon(); } } // 表示を更新 this.updateDisplay(); } updateDisplay() { for (let row = 0; row < 12; row++) { for (let col = 0; col < 6; col++) { const cell = document.querySelector(`[data-row="${row}"][data-col="${col}"]`); this.updateCellDisplay(cell, this.board[row][col]); } } } startGame() { // スタート画面を非表示、ゲーム画面を表示 document.getElementById('startScreen').style.display = 'none'; document.getElementById('gameScreen').classList.add('active'); this.gameActive = true; this.score = 0; this.timeLeft = 60; document.getElementById('score').textContent = this.score; document.getElementById('timer').textContent = this.timeLeft; this.timer = setInterval(() => { this.timeLeft--; document.getElementById('timer').textContent = this.timeLeft; if (this.timeLeft <= 0) { this.endGame(); } }, 1000); this.createBoard(); } endGame() { this.gameActive = false; clearInterval(this.timer); document.getElementById('finalScore').textContent = this.score; document.getElementById('gameOver').style.display = 'flex'; } } let game; function startNewGame() { document.getElementById('gameOver').style.display = 'none'; if (!game) { game = new AWSPuzzleGame(); } game.startGame(); } function backToStart() { if (game) { game.gameActive = false; if (game.timer) clearInterval(game.timer); } document.getElementById('gameScreen').classList.remove('active'); document.getElementById('startScreen').style.display = 'flex'; document.getElementById('gameOver').style.display = 'none'; } // ページ読み込み時にゲームを初期化 window.addEventListener('load', () => { game = new AWSPuzzleGame(); // デフォルトで簡単レベルを選択 document.querySelector('[data-level="easy"]').classList.add('selected'); game.setDifficulty('easy'); }); </script> </body> </html>
白鳥さんのサポートがあり、この記事を完成させることができました。ありがとうございます!