Amazon Q Developer for CLIを使って、自然言語オンリーでモグラ叩きゲームを作ったよ、という話

今日はJAWS-UG佐賀さんのイベント、 Amazon Q Developer for CLI De GameをつくらNight に参加して、気になっていた Amazon Q Developer for CLI でゲーム作成してきました。

完成品はこちら

Githubリポジトリ

ひたすら自然言語のみ、という縛りで、40分余りでこれが完成してしまいました。恐るべし。

Amazon Q Developer for CLIって何?

AWSが提供している、AIによるコーディングアシスタントです。 インストールすれば、コマンドラインから利用が可能です。 Claude4.0sonnetが使えて、無料(2025.6.11現在)でエージェントモードでバリバリコーディングしてくれます。 太っ腹すぎる!!!

今回やったこと

1.AWS Builder IDを取得(設定に必要)

2.Amazon Q Developer CLIのインストール

3.ターミナルからAmazon Q Developer CLIを呼び出す。

q chat

で呼び出すだけです。 あとは素材だけ与えて、 「モグラ叩き作って」 「点数システム変えて」 みたいな感じで仕様を詰めていきました。

概要もまとめてもらいました。

葉隠モグラ叩き塾(Hagakure Mogura)

製品概要

「葉隠モグラ叩き塾」は、日本の江戸時代をテーマにした伝統的なモグラ叩きゲームのウェブアプリケーションです。プレイヤーは制限時間内に画面から出現する様々なキャラクターを叩いて 高得点を目指します。

主な特徴

和風テーマ: 侍、忍者、町娘などの江戸時代風キャラクターが登場 • コンボシステム: プラスポイントを連続で獲得するとコンボが増加し、得点倍率がアップ • レベル進行: プレイ時間の経過とともにレベルが上がり、キャラクターの出現速度が上昇 • 多様なキャラクター: 異なる得点価値を持つ4種類のキャラクター • レスポンシブデザイン: スマートフォンからデスクトップまで様々なデバイスに対応

キャラクター

  1. 侍: +50点(レア出現)
  2. 忍者(男): +10点(高頻度で出現)
  3. 忍者(女): -20点(中頻度で出現)
  4. 町娘: -100点(レア出現、大きなペナルティ)

ゲームシステム

制限時間: 60秒 • コンボ倍率: 10コンボごとに倍率が上昇(×2、×3、×4...) • レベルアップ: 10秒ごとにレベルが上昇し、キャラクターの出現速度が増加 • ペナルティ: マイナスポイントのキャラクターを叩くとコンボがリセット

技術仕様

開発言語: HTML, CSS, JavaScript • 外部依存性: なし(純粋なフロントエンド実装) • ファイルサイズ: 約22KB(HTML)、画像ファイル複数 • デプロイ要件: 標準的なウェブサーバー

ビジュアル要素

• 和風の背景デザイン • 城と木のフッターイラスト • 伝統的な日本風のキャラクターイラスト • 得点エフェクトとコンボ表示アニメーション

今後の展開予定

• 追加キャラクターの実装 • 難易度選択機能 • ハイスコアの保存と共有機能 • サウンドエフェクトと背景音楽の追加

このゲームは、伝統的な「モグラ叩き」のゲームプレイに日本の歴史的要素を組み合わせた、シンプルながらも中毒性のある娯楽体験を提供します。

MCPもいけるみたい。とりあえずエージェントやってみたい人は、無料で行けるのはこれ一択なんじゃないでしょうか。

以下作らせたソースコード全文

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>葉隠モグラ - Hagakure Mogura</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: 'Arial', sans-serif;
            background: linear-gradient(180deg, #e8f4f8 0%, #d4e8d4 50%, #c8dcc8 100%);
            background-size: cover;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
            position: relative;
        }

        body::before {
            content: '';
            position: fixed;
            bottom: 0;
            right: 20px;
            width: 200px;
            height: 150px;
            background: url('images/footer_castle.png') no-repeat center bottom;
            background-size: contain;
            opacity: 0.2;
            pointer-events: none;
            z-index: -1;
        }



        .game-title {
            font-size: 2.2em;
            color: #8B4513;
            text-shadow: 
                2px 2px 4px rgba(0,0,0,0.8),
                -1px -1px 2px rgba(255,255,255,0.9);
            margin: 20px 0;
            font-weight: bold;
            background: rgba(255,255,255,0.9);
            padding: 30px 80px;
            border-radius: 20px;
            border: 3px solid #8B4513;
            position: relative;
        }

        .game-title::before {
            content: '';
            position: absolute;
            left: 15px;
            top: 50%;
            transform: translateY(-50%);
            width: 50px;
            height: 50px;
            background: url('images/logo.png') no-repeat center;
            background-size: contain;
        }

        .game-title::after {
            content: '';
            position: absolute;
            right: 15px;
            top: 50%;
            transform: translateY(-50%);
            width: 50px;
            height: 50px;
            background: url('images/logo.png') no-repeat center;
            background-size: contain;
        }

        .game-info {
            display: flex;
            gap: 20px;
            margin-bottom: 20px;
            font-size: 1.2em;
            color: #654321;
            flex-wrap: wrap;
            justify-content: center;
        }

        .score, .time, .level, .combo {
            background: rgba(255,255,255,0.9);
            padding: 10px 20px;
            border-radius: 10px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            border: 2px solid #8B4513;
        }

        .combo {
            background: linear-gradient(45deg, #FFD700, #FFA500);
            color: #8B4513;
            font-weight: bold;
        }

        .combo.high-combo {
            background: linear-gradient(45deg, #FF6B6B, #FF1493);
            color: white;
            animation: comboGlow 0.5s ease-in-out infinite alternate;
        }

        @keyframes comboGlow {
            0% { box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
            100% { box-shadow: 0 4px 15px rgba(255,107,107,0.6); }
        }

        .character-guide {
            display: flex;
            gap: 15px;
            margin-bottom: 20px;
            flex-wrap: wrap;
            justify-content: center;
        }

        .character-info {
            background: rgba(255,255,255,0.9);
            padding: 10px;
            border-radius: 10px;
            text-align: center;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            min-width: 100px;
            border: 2px solid #8B4513;
        }

        .character-info img {
            width: 40px;
            height: 40px;
            object-fit: contain;
            display: block;
            margin: 0 auto 5px;
        }

        .character-info .points {
            font-weight: bold;
            font-size: 1.1em;
        }

        .positive-points {
            color: #2E8B57;
        }

        .negative-points {
            color: #DC143C;
        }

        .character-info .name {
            font-size: 0.9em;
            color: #666;
        }

        .game-board {
            display: grid;
            grid-template-columns: repeat(3, 150px);
            grid-template-rows: repeat(3, 150px);
            gap: 20px;
            background: rgba(139, 69, 19, 0.1);
            padding: 30px;
            border-radius: 20px;
            box-shadow: 
                0 5px 15px rgba(0,0,0,0.3),
                inset 0 2px 10px rgba(255,255,255,0.2);
            border: 4px solid #8B4513;
        }

        .hole {
            width: 150px;
            height: 150px;
            background: #8B4513;
            border-radius: 50%;
            position: relative;
            cursor: pointer;
            box-shadow: inset 0 5px 10px rgba(0,0,0,0.5);
            overflow: hidden;
            transition: transform 0.1s;
            /* タッチデバイス対応 */
            -webkit-tap-highlight-color: transparent;
            user-select: none;
        }

        .hole:hover {
            transform: scale(1.05);
        }

        .hole:active {
            transform: scale(0.95);
        }

        .character {
            position: absolute;
            width: 120px;
            height: 120px;
            bottom: -120px;
            left: 50%;
            transform: translateX(-50%);
            transition: bottom 0.3s ease-in-out;
            cursor: pointer;
        }

        .character.show {
            bottom: 10px;
        }

        .character img {
            width: 100%;
            height: 100%;
            object-fit: contain;
        }

        .start-button, .restart-button {
            background: linear-gradient(45deg, #8B4513, #D2691E);
            color: white;
            border: 3px solid #654321;
            padding: 15px 30px;
            font-size: 1.2em;
            border-radius: 25px;
            cursor: pointer;
            margin: 20px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.3);
            transition: transform 0.2s;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
            font-weight: bold;
            /* タッチデバイス対応 */
            -webkit-tap-highlight-color: transparent;
            user-select: none;
        }

        .start-button:hover, .restart-button:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 12px rgba(0,0,0,0.4);
        }

        .start-button:active, .restart-button:active {
            transform: translateY(0px);
            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
        }

        .game-over {
            display: none;
            text-align: center;
            background: rgba(255,255,255,0.95);
            padding: 30px;
            border-radius: 15px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.3);
            border: 4px solid #8B4513;
        }

        .hit-effect {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #FF6B6B;
            font-size: 2em;
            font-weight: bold;
            pointer-events: none;
            animation: hitAnimation 0.5s ease-out;
        }

        @keyframes hitAnimation {
            0% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
            100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5); }
        }

        /* レスポンシブデザイン - スマホ対応 */
        @media (max-width: 768px) {
            .game-title {
                font-size: 1.5em;
                padding: 20px 60px;
                margin: 10px;
            }

            .game-title::before,
            .game-title::after {
                width: 35px;
                height: 35px;
                left: 10px;
                right: 10px;
            }

            .game-info {
                gap: 10px;
                font-size: 1em;
                margin-bottom: 15px;
            }

            .score, .time, .level, .combo {
                padding: 8px 12px;
                font-size: 0.9em;
            }

            .character-guide {
                gap: 8px;
                margin-bottom: 15px;
            }

            .character-info {
                padding: 8px;
                min-width: 80px;
            }

            .character-info img {
                width: 30px;
                height: 30px;
            }

            .character-info .name {
                font-size: 0.8em;
            }

            .character-info .points {
                font-size: 1em;
            }

            .game-board {
                grid-template-columns: repeat(3, 100px);
                grid-template-rows: repeat(3, 100px);
                gap: 15px;
                padding: 20px;
                margin: 0 10px;
            }

            .hole {
                width: 100px;
                height: 100px;
            }

            .character {
                width: 80px;
                height: 80px;
                bottom: -80px;
            }

            .character.show {
                bottom: 5px;
            }

            .start-button, .restart-button {
                padding: 12px 24px;
                font-size: 1em;
                margin: 15px 10px;
            }

            .game-over {
                margin: 0 20px;
                padding: 20px;
            }

            body::before {
                width: 120px;
                height: 90px;
                right: 10px;
            }
        }

        @media (max-width: 480px) {
            .game-title {
                font-size: 1.2em;
                padding: 15px 50px;
            }

            .game-title::before,
            .game-title::after {
                width: 25px;
                height: 25px;
            }

            .game-info {
                flex-direction: column;
                align-items: center;
                gap: 8px;
            }

            .character-guide {
                flex-direction: column;
                align-items: center;
                gap: 5px;
            }

            .character-info {
                display: flex;
                align-items: center;
                gap: 10px;
                min-width: 200px;
                text-align: left;
            }

            .character-info img {
                width: 25px;
                height: 25px;
                margin: 0;
            }

            .game-board {
                grid-template-columns: repeat(3, 80px);
                grid-template-rows: repeat(3, 80px);
                gap: 10px;
                padding: 15px;
            }

            .hole {
                width: 80px;
                height: 80px;
            }

            .character {
                width: 65px;
                height: 65px;
                bottom: -65px;
            }

            .character.show {
                bottom: 3px;
            }

            body::before {
                display: none;
            }
        }
    </style>
</head>
<body>
    <h1 class="game-title">HAGAKURE モグラ叩き塾</h1>
    
    <div style="text-align: center; margin-bottom: 15px; color: #654321; font-size: 0.9em;">
        <strong>コンボシステム:</strong> プラスポイントを連続で獲得すると倍率アップ!<br>
        10コンボ毎に得点が×2, ×3, ×4...と倍増。マイナスポイントでコンボリセット。
    </div>
    
    <div class="character-guide">
        <div class="character-info">
            <img src="images/voice-samurai.png" alt="侍">
            <div class="name">侍</div>
            <div class="points positive-points">+50点</div>
        </div>
        <div class="character-info">
            <img src="images/voice-ninja1.png" alt="忍者(男)">
            <div class="name">忍者(男)</div>
            <div class="points positive-points">+10点</div>
        </div>
        <div class="character-info">
            <img src="images/voice-ninja2.png" alt="忍者(女)">
            <div class="name">忍者(女)</div>
            <div class="points negative-points">-20点</div>
        </div>
        <div class="character-info">
            <img src="images/machi-musume.png" alt="町娘">
            <div class="name">町娘</div>
            <div class="points negative-points">-100点</div>
        </div>
    </div>
    
    <div class="game-info">
        <div class="score">スコア: <span id="score">0</span></div>
        <div class="time">時間: <span id="time">60</span>秒</div>
        <div class="level">レベル: <span id="level">1</span></div>
        <div class="combo" id="comboDisplay">コンボ: <span id="combo">0</span> (×<span id="multiplier">1</span>)</div>
    </div>

    <div class="game-board" id="gameBoard">
        <!-- 穴は JavaScript で生成 -->
    </div>

    <button class="start-button" id="startButton" onclick="startGame()">ゲーム開始</button>
    
    <div class="game-over" id="gameOver">
        <h2>ゲーム終了!</h2>
        <p>最終スコア: <span id="finalScore">0</span></p>
        <button class="restart-button" onclick="restartGame()">もう一度プレイ</button>
    </div>

    <script>
        // ゲーム変数
        let score = 0;
        let timeLeft = 60;
        let level = 1;
        let gameActive = false;
        let gameTimer;
        let spawnTimer;
        let currentSpeed = 800; // キャラクター出現間隔(ミリ秒)
        let combo = 0; // コンボカウント
        let multiplier = 1; // 得点倍率

        // キャラクターごとのポイントと出現頻度
        const characterData = {
            'voice-samurai.png': { points: 50, weight: 1 },      // 侍: 50点, 時々出現
            'voice-ninja1.png': { points: 10, weight: 5 },      // 忍者(男): 10点, 頻出
            'voice-ninja2.png': { points: -20, weight: 3 },     // 忍者(女): -20点, まあまあ出現
            'machi-musume.png': { points: -100, weight: 1 }     // 町娘: -100点, 時々出現
        };

        // 重み付きランダム選択用の配列を作成
        function createWeightedCharacterArray() {
            const weightedArray = [];
            Object.keys(characterData).forEach(character => {
                const weight = characterData[character].weight;
                for (let i = 0; i < weight; i++) {
                    weightedArray.push(character);
                }
            });
            return weightedArray;
        }

        const weightedCharacters = createWeightedCharacterArray();

        // ゲームボード初期化
        function initializeBoard() {
            const gameBoard = document.getElementById('gameBoard');
            gameBoard.innerHTML = '';
            
            for (let i = 0; i < 9; i++) {
                const hole = document.createElement('div');
                hole.className = 'hole';
                hole.id = `hole-${i}`;
                
                const character = document.createElement('div');
                character.className = 'character';
                character.id = `character-${i}`;
                character.onclick = () => hitCharacter(i);
                
                const img = document.createElement('img');
                character.appendChild(img);
                hole.appendChild(character);
                gameBoard.appendChild(hole);
            }
        }

        // ゲーム開始
        function startGame() {
            gameActive = true;
            score = 0;
            timeLeft = 60;
            level = 1;
            currentSpeed = 800;
            combo = 0;
            multiplier = 1;
            
            updateDisplay();
            document.getElementById('startButton').style.display = 'none';
            document.getElementById('gameOver').style.display = 'none';
            
            // タイマー開始
            gameTimer = setInterval(() => {
                timeLeft--;
                updateDisplay();
                
                // レベルアップ処理(より頻繁に速度アップ)
                if (timeLeft % 10 === 0 && timeLeft > 0) {
                    level++;
                    currentSpeed = Math.max(200, currentSpeed - 100);
                    updateDisplay();
                }
                
                if (timeLeft <= 0) {
                    endGame();
                }
            }, 1000);
            
            // キャラクター出現開始
            spawnCharacters();
        }

        // キャラクター出現
        function spawnCharacters() {
            if (!gameActive) return;
            
            // 複数のキャラクターを同時に出現させる可能性
            const spawnCount = Math.random() < 0.4 ? 2 : 1; // 40%の確率で2体同時出現
            
            for (let i = 0; i < spawnCount; i++) {
                const randomHole = Math.floor(Math.random() * 9);
                // 重み付きランダム選択
                const randomCharacter = 'images/' + weightedCharacters[Math.floor(Math.random() * weightedCharacters.length)];
                
                showCharacter(randomHole, randomCharacter);
            }
            
            // ランダム要素を減らしてより頻繁に出現
            spawnTimer = setTimeout(spawnCharacters, currentSpeed + Math.random() * 300);
        }

        // キャラクター表示
        function showCharacter(holeIndex, characterImage) {
            const character = document.getElementById(`character-${holeIndex}`);
            const img = character.querySelector('img');
            
            // 既に表示されている場合はスキップ
            if (character.classList.contains('show')) return;
            
            img.src = characterImage;
            character.classList.add('show');
            character.dataset.character = characterImage.split('/').pop();
            
            // 表示時間を短縮してよりスピーディに
            setTimeout(() => {
                character.classList.remove('show');
            }, 1000 + Math.random() * 800);
        }

        // キャラクターを叩く
        function hitCharacter(holeIndex) {
            const character = document.getElementById(`character-${holeIndex}`);
            
            if (!gameActive || !character.classList.contains('show')) return;
            
            const characterType = character.dataset.character;
            const basePoints = characterData[characterType]?.points || 0;
            
            if (basePoints > 0) {
                // プラスポイントの場合:コンボ継続、倍率適用
                combo++;
                multiplier = Math.floor(combo / 10) + 1;
                const finalPoints = basePoints * multiplier;
                score += finalPoints;
                
                // コンボエフェクト
                showHitEffect(holeIndex, `+${finalPoints} (×${multiplier})`, true);
                updateComboDisplay();
            } else {
                // マイナスポイントの場合:コンボリセット、倍率リセット
                combo = 0;
                multiplier = 1;
                score += basePoints; // マイナスポイントはそのまま
                
                // ペナルティエフェクト
                showHitEffect(holeIndex, `${basePoints} COMBO RESET!`, false);
                updateComboDisplay();
            }
            
            character.classList.remove('show');
            updateDisplay();
        }

        // コンボ表示更新
        function updateComboDisplay() {
            const comboElement = document.getElementById('comboDisplay');
            if (combo >= 20) {
                comboElement.classList.add('high-combo');
            } else {
                comboElement.classList.remove('high-combo');
            }
        }

        // ヒットエフェクト表示
        function showHitEffect(holeIndex, text, isPositive) {
            const hole = document.getElementById(`hole-${holeIndex}`);
            const effect = document.createElement('div');
            effect.className = 'hit-effect';
            effect.textContent = text;
            effect.style.color = isPositive ? '#2E8B57' : '#DC143C';
            hole.appendChild(effect);
            
            setTimeout(() => {
                hole.removeChild(effect);
            }, 500);
        }

        // 表示更新
        function updateDisplay() {
            document.getElementById('score').textContent = score;
            document.getElementById('time').textContent = timeLeft;
            document.getElementById('level').textContent = level;
            document.getElementById('combo').textContent = combo;
            document.getElementById('multiplier').textContent = multiplier;
        }

        // ゲーム終了
        function endGame() {
            gameActive = false;
            clearInterval(gameTimer);
            clearTimeout(spawnTimer);
            
            // 全キャラクターを隠す
            for (let i = 0; i < 9; i++) {
                document.getElementById(`character-${i}`).classList.remove('show');
            }
            
            document.getElementById('finalScore').textContent = score;
            document.getElementById('gameOver').style.display = 'block';
        }

        // ゲーム再開
        function restartGame() {
            document.getElementById('startButton').style.display = 'inline-block';
            document.getElementById('gameOver').style.display = 'none';
        }

        // 初期化
        window.onload = function() {
            initializeBoard();
        };
    </script>
</body>
</html>