メインコンテンツへスキップ

AI app きしめんキャッチャーゲームアプリ

    Google Japanのラーメンキャッチャーゲームアプリをきしめん仕様に変更したきしめんキャッチャーゲームアプリです。

    Code

    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>きしめんキャッチャーゲーム</title>
        <script src="https://cdn.tailwindcss.com"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
        <style>
            @import url('https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@700&family=Noto+Sans+JP:wght@400;700&display=swap');
            body {
                font-family: 'Noto Sans JP', 'M PLUS Rounded 1c', sans-serif;
                touch-action: manipulation; /* Changed from none to allow scrolling */
            }
            .ramen-font {
                font-family: 'M PLUS Rounded 1c', sans-serif;
            }
            canvas {
                background-image: url('https://www.transparenttextures.com/patterns/ramie.png');
                background-color: #f5f5f5 ;
                cursor: grab;
            }
            canvas:active {
                cursor: grabbing;
            }
            .result-ramen-container {
                position: relative;
                width: 200px;
                height: 200px;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .result-bowl {
                font-size: 150px;
                line-height: 1;
            }
            .result-ingredient {
                position: absolute;
                font-size: 35px;
                animation: appear 0.5s ease-in-out;
            }
            @keyframes appear {
                0% { transform: scale(0) rotate(-180deg); opacity: 0; }
                100% { transform: scale(1) rotate(0deg); opacity: 1; }
            }
            .button-shadow {
                box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06), 0 8px 10px 0px rgba(249, 115, 22, 0.2);
            }
            .button-shadow:hover {
                box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05), 0 12px 16px 0px rgba(249, 115, 22, 0.3);
            }
            .star-rating {
                color: #ffc107 ;
                letter-spacing: 2px;
            }
            .loader {
                border: 4px solid #f3f3f3 ;
                border-top: 4px solid #8B5CF6 ;
                border-radius: 50%;
                width: 40px;
                height: 40px;
                animation: spin 1s linear infinite;
            }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
            .shake-animation {
                animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
            }
            @keyframes shake {
              10%, 90% { transform: translate3d(-1px, 0, 0); }
              20%, 80% { transform: translate3d(2px, 0, 0); }
              30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
              40%, 60% { transform: translate3d(4px, 0, 0); }
            }
            
            /* Notification Styles */
            @keyframes slide-in-right {
                from { transform: translateX(100%); opacity: 0; }
                to { transform: translateX(0); opacity: 1; }
            }
            @keyframes fade-out {
                from { opacity: 1; }
                to { opacity: 0; }
            }
            .animate-slide-in-right { animation: slide-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; }
            .animate-fade-out { animation: fade-out 0.5s ease-out both; animation-delay: 3s;}
    
        </style>
    </head>
    <body class="bg-orange-50 flex items-center justify-center min-h-screen">
    
        <div id="game-container" class="w-full max-w-lg mx-auto p-4">
            
            <div id="start-screen" class="text-center">
                <h1 class="text-5xl font-bold text-orange-800 mb-2 ramen-font">きしめんキャッチャーゲーム</h1>
                <p class="text-orange-600 mb-6">器を動かして、Gemini評論家を唸らせろ!</p>
                <div class="text-8xl mb-6">🍲</div>
                <button id="start-button" class="bg-orange-500 hover:bg-orange-600 text-white font-bold py-4 px-8 rounded-full text-2xl transition-all duration-300 transform hover:scale-105 button-shadow ramen-font">いざ、勝負!</button>
                 <div class="mt-8 text-sm text-gray-500">
                    <p>操作方法:</p>
                    <p>PC: ← → キー / マウス</p>
                    <p>スマホ: 画面をスワイプ</p>
                </div>
            </div>
    
            <div id="game-screen" class="hidden text-center">
                <div class="flex justify-between items-center mb-2 text-2xl font-bold text-orange-700">
                    <div>スコア: <span id="score">0</span></div>
                    <div>残り時間: <span id="timer">20</span></div>
                </div>
                <canvas id="game-canvas" class="border-4 border-orange-300 rounded-lg shadow-lg"></canvas>
            </div>
    
            <div id="result-screen" class="hidden text-center p-6 bg-white rounded-2xl shadow-2xl border-4 border-orange-200">
                
                <div id="share-content" class="bg-white">
                    <h2 class="text-4xl font-bold text-orange-800 mb-2 ramen-font">審判の時...!</h2>
                    <div class="flex flex-col md:flex-row items-center justify-around gap-4">
                        <div>
                            <div id="result-ramen" class="result-ramen-container mx-auto">
                                <div class="result-bowl">🍲</div>
                                <div id="toppings-container"></div>
                            </div>
                            <p id="ramen-name" class="text-xl md:text-2xl font-bold text-indigo-700 mt-2 h-16 flex items-center justify-center ramen-font"></p>
                        </div>
                        <div class="text-left bg-orange-50 p-4 rounded-lg w-full md:w-auto">
                            <h3 class="text-lg font-bold text-orange-700 mb-2">きしめん分析結果</h3>
                            <p class="text-lg">スコア: <span id="final-score" class="font-black">0</span> 点</p>
                            <p class="text-lg">総カロリー: <span id="total-calories" class="font-black">0</span> kcal</p>
                            <p class="text-lg flex items-center">Gemini感情評価: <span id="total-tastiness-stars" class="font-black star-rating text-2xl ml-2">評価中...</span></p>
                        </div>
                    </div>
                    <div class="mt-6 text-left">
                        <h3 class="text-lg font-bold text-gray-700 mb-2 text-center">感情爆発Geminiレビュー</h3>
                        <div id="ai-reviews-container" class="space-y-3 p-4 bg-gray-100 rounded-lg border border-gray-300 min-h-[150px]"></div>
                    </div>
                </div>
                
                <div class="mt-4">
                    <h3 class="text-lg font-bold text-orange-700 mb-2">ゲットした具材リスト</h3>
                    <div class="max-h-40 overflow-y-auto bg-gray-50 rounded-lg p-2 border">
                        <table class="w-full text-sm text-left">
                            <thead class="text-xs text-orange-800 uppercase bg-orange-100">
                                <tr><th scope="col" class="px-2 py-2">具材</th><th scope="col" class="px-2 py-2">旨さ</th><th scope="col" class="px-2 py-2">カロリー</th><th scope="col" class="px-2 py-2">おすすめ度</th></tr>
                            </thead>
                            <tbody id="ingredient-details-body"></tbody>
                        </table>
                    </div>
                </div>
    
                <div class="flex flex-col sm:flex-row gap-4 mt-6">
                    <button id="restart-button" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-bold py-3 px-8 rounded-full text-xl transition-all duration-300 transform hover:scale-105 button-shadow ramen-font">もう一杯!</button>
                    <button id="share-button" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-8 rounded-full text-xl transition-all duration-300 transform hover:scale-105 button-shadow ramen-font flex items-center justify-center gap-2 disabled:bg-blue-300">
                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>
                        <span id="share-button-text">結果をシェア</span>
                    </button>
                </div>
                
                <div id="takeaway-section" class="mt-8 p-4 bg-indigo-50 border-2 border-dashed border-indigo-200 rounded-lg text-left">
                    <h3 class="text-xl font-bold text-indigo-800 mb-2 ramen-font flex items-center">
                        <span class="text-2xl mr-2">🤖</span> こんな指示で、GoogleのAI、Geminiが作りました。
                    </h3>
                    <div class="relative">
                        <textarea id="prompt-textarea" class="w-full h-32 p-2 border border-gray-300 rounded-lg text-sm bg-gray-50" readonly>HTML、CSS、JavaScriptで「きしめんキャッチャーゲーム」を作ってください。
    
    - 器を左右に動かして、上から落ちてくる具材をキャッチする。
    - 20秒の制限時間を設ける。
    - ゲーム終了後、結果画面を表示する。
    - 結果画面では、キャッチした具材に応じて、GoogleのAI「Gemini」がユニークなきしめん名を命名し、感情豊かなレビューを生成する。
    - 全体的に楽しく、ポップなデザインにする。</textarea>
                        <button id="copy-prompt-button" class="absolute top-2 right-2 bg-gray-600 hover:bg-gray-700 text-white text-xs font-bold py-1 px-2 rounded">コピー</button>
                    </div>
                    <div class="mt-4 text-center">
                        <a href="https://gemini.google.com/" target="_blank" class="inline-block bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-full text-lg transition-all duration-300 transform hover:scale-105 button-shadow">
                            Geminiを起動して今すぐ自分だけのゲームを作る!
                        </a>
                    </div>
                </div>
            </div>
        </div>
        
        <!-- Notification Area -->
        <div id="notification-area" class="fixed bottom-5 right-5 space-y-2 z-50 w-full max-w-xs"></div>
    
        <script>
        document.addEventListener('DOMContentLoaded', function() {
            // --- DOM要素の取得 ---
            const gameContainer = document.getElementById('game-container');
            const startScreen = document.getElementById('start-screen');
            const gameScreen = document.getElementById('game-screen');
            const resultScreen = document.getElementById('result-screen');
            const startButton = document.getElementById('start-button');
            const restartButton = document.getElementById('restart-button');
            const shareButton = document.getElementById('share-button');
            const shareButtonText = document.getElementById('share-button-text');
            const canvas = document.getElementById('game-canvas');
            const ctx = canvas.getContext('2d');
            const scoreDisplay = document.getElementById('score');
            const timerDisplay = document.getElementById('timer');
            const finalScoreDisplay = document.getElementById('final-score');
            const totalCaloriesDisplay = document.getElementById('total-calories');
            const totalTastinessStarsDisplay = document.getElementById('total-tastiness-stars');
            const toppingsContainer = document.getElementById('toppings-container');
            const ramenNameDisplay = document.getElementById('ramen-name');
            const ingredientDetailsBody = document.getElementById('ingredient-details-body');
            const aiReviewsContainer = document.getElementById('ai-reviews-container');
            const copyPromptButton = document.getElementById('copy-prompt-button');
            const promptTextarea = document.getElementById('prompt-textarea');
            const notificationArea = document.getElementById('notification-area');
    
            // --- ゲーム設定 ---
            const GAME_DURATION = 20;
            let timer, score, gameInterval, timeLeft;
            let caughtIngredients = [];
            let lastKishimenName = '';
            let lastKishimenAvgStars = '';
            const player = { width: 80, height: 40, x: 0, y: 0, speed: 25, emoji: '🍲', fontSize: 60 };
    
            const ingredients = [
                { emoji: '🍖', name: '極上チャーシュー', score: 35, tastiness: 10, calories: 150, recommendation: 5 },
                { emoji: '🥚', name: 'とろとろ味玉', score: 25, tastiness: 9, calories: 90, recommendation: 5 },
                { emoji: '🍥', name: 'かまぼこ', score: 10, tastiness: 5, calories: 20, recommendation: 3 },
                { emoji: '🥬', name: 'ほうれん草', score: 15, tastiness: 6, calories: 10, recommendation: 4 },
                { emoji: '🌽', name: '甘々コーン', score: 15, tastiness: 7, calories: 40, recommendation: 4 },
                { emoji: '油揚げ', name: 'じゅわっと油揚げ', score: 20, tastiness: 8, calories: 80, recommendation: 5 },
                { emoji: '🌶️', name: '激辛唐辛子', score: 15, tastiness: 7, calories: 5, recommendation: 3 },
                { emoji: '🧄', name: 'マシマシにんにく', score: 25, tastiness: 9, calories: 15, recommendation: 5 },
                { emoji: '🍤', name: 'サクサクエビ天', score: 40, tastiness: 10, calories: 120, recommendation: 5 },
                { emoji: '🧀', name: 'とろけるチーズ', score: 25, tastiness: 8, calories: 110, recommendation: 4 },
                { emoji: '🍄', name: '味染みしいたけ', score: 15, tastiness: 7, calories: 15, recommendation: 4 },
                { emoji: '🎋', name: '極太メンマ', score: 15, tastiness: 7, calories: 25, recommendation: 4},
                { emoji: '🧅', name: '山盛りネギ', score: 15, tastiness: 8, calories: 10, recommendation: 5},
                { emoji: '⚫', name: '高級海苔', score: 10, tastiness: 7, calories: 5, recommendation: 4},
                { emoji: '💣', name: '爆弾', score: -50, tastiness: 0, calories: 0, recommendation: 0 },
                { emoji: '🌿', name: 'きざみねぎ', score: 10, tastiness: 7, calories: 5, recommendation: 4 }
            ];
            let fallingIngredients = [];
    
            function init() {
                // Set styles for game and start screen (centered)
                document.body.classList.add('items-center');
                document.body.style.overflow = 'hidden';
                
                const container = document.getElementById('game-container');
                canvas.width = container.clientWidth > 0 ? container.clientWidth - 20 : 300;
                canvas.height = window.innerHeight * 0.6;
                player.x = (canvas.width - player.width) / 2;
                player.y = canvas.height - player.height - 10;
                score = 0; timeLeft = GAME_DURATION;
                fallingIngredients = []; caughtIngredients = [];
                scoreDisplay.textContent = score;
                timerDisplay.textContent = timeLeft;
                toppingsContainer.innerHTML = '';
                ingredientDetailsBody.innerHTML = '';
                ramenNameDisplay.textContent = '';
                startScreen.classList.add('hidden');
                resultScreen.classList.add('hidden');
                gameScreen.classList.remove('hidden');
                startGameLoop();
            }
    
            function startGameLoop() {
                timer = setInterval(() => {
                    timeLeft--;
                    timerDisplay.textContent = timeLeft;
                    if (timeLeft <= 0) endGame();
                }, 1000);
                gameInterval = setInterval(updateGame, 1000 / 60);
            }
    
            function shakeScreen() {
                gameContainer.classList.add('shake-animation');
                setTimeout(() => gameContainer.classList.remove('shake-animation'), 500);
            }
    
            function updateGame() {
                if (Math.random() < 0.07) {
                    const ing = ingredients[Math.floor(Math.random() * ingredients.length)];
                    fallingIngredients.push({ ...ing, x: Math.random() * (canvas.width - 40), y: -40, speed: 2 + Math.random() * 3, fontSize: 40 });
                }
    
                for (let i = fallingIngredients.length - 1; i >= 0; i--) {
                    const ing = fallingIngredients[i];
                    ing.y += ing.speed;
                    if (ing.y > canvas.height) {
                        fallingIngredients.splice(i, 1);
                        continue;
                    }
                    if (ing.x < player.x + player.width && ing.x + ing.fontSize > player.x && ing.y < player.y + player.height && ing.y + ing.fontSize > player.y) {
                        score += ing.score;
                        caughtIngredients.push(ing);
                        if (ing.name === '爆弾') {
                            shakeScreen();
                        }
                        fallingIngredients.splice(i, 1);
                    }
                }
                drawGame();
            }
    
            function drawGame() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                scoreDisplay.textContent = score;
                ctx.font = `${player.fontSize}px sans-serif`; ctx.textAlign = 'center';
                ctx.fillText(player.emoji, player.x + player.width / 2, player.y + player.fontSize / 2 + 10);
                fallingIngredients.forEach(ing => {
                    ctx.font = `${ing.fontSize}px sans-serif`;
                    ctx.fillText(ing.emoji, ing.x + ing.fontSize / 2, ing.y + ing.fontSize / 2);
                });
            }
            
            async function endGame() {
                // Set styles for result screen (scrollable)
                document.body.classList.remove('items-center');
                document.body.style.overflow = 'auto';
                
                clearInterval(gameInterval);
                clearInterval(timer);
                gameScreen.classList.add('hidden');
                resultScreen.classList.remove('hidden');
                await generateResult();
            }
    
            function generateStars(rating, max = 5) {
                rating = Math.round(rating); return '★'.repeat(rating) + '☆'.repeat(max - rating);
            }
            
            function showNotification(message, isError = false) {
                const notification = document.createElement('div');
                const bgColor = isError ? 'bg-red-500' : 'bg-indigo-500';
                notification.className = `${bgColor} text-white text-sm font-bold py-2 px-4 rounded-lg shadow-xl transform animate-slide-in-right`;
                notification.textContent = message;
                notificationArea.appendChild(notification);
                notification.classList.add('animate-fade-out');
                notification.addEventListener('animationend', (e) => {
                    if (e.animationName === 'fade-out') {
                        notification.remove();
                    }
                });
            }
    
            async function generateResult() {
                // Setup loading UI
                const totalCalories = caughtIngredients.reduce((sum, ing) => sum + ing.calories, 0);
                finalScoreDisplay.textContent = score;
                totalCaloriesDisplay.textContent = totalCalories;
                generateResultRamenView();
                shareButton.disabled = true;
    
                // Display loading indicators
                ramenNameDisplay.innerHTML = `<div class="loader !w-6 !h-6 !border-2"></div><span class="ml-2">Geminiが命名中...</span>`;
                totalTastinessStarsDisplay.textContent = '評価中...';
                totalTastinessStarsDisplay.classList.remove('star-rating');
                aiReviewsContainer.innerHTML = `<div class="flex flex-col items-center justify-center p-4"><div class="loader"></div><p class="mt-3 text-gray-600">Gemini評論家たちが感情を昂らせています...</p></div>`;
                
                // Fetch AI content
                const resultData = { score, totalCalories, caughtIngredients };
                const aiContent = await fetchAiNameAndReviews(resultData);
    
                // Update UI with content
                if (aiContent) {
                    lastKishimenName = aiContent.kishimen_name;
                    ramenNameDisplay.textContent = `『${lastKishimenName}』`;
    
                    aiReviewsContainer.innerHTML = '';
                    let totalStars = 0;
                    aiContent.reviews.forEach(review => {
                        const reviewEl = document.createElement('div');
                        reviewEl.className = 'p-3 bg-white rounded-md border';
                        reviewEl.innerHTML = `<div class="flex justify-between items-center"><p class="font-bold text-purple-700">${review.reviewer_name}</p><p class="star-rating text-lg">${generateStars(review.stars)}</p></div><p class="mt-1 text-gray-800 italic">"${review.review_text}"</p>`;
                        aiReviewsContainer.appendChild(reviewEl);
                        totalStars += review.stars;
                    });
                    
                    const avgRating = aiContent.reviews.length > 0 ? totalStars / aiContent.reviews.length : 0;
                    lastKishimenAvgStars = generateStars(avgRating);
                    totalTastinessStarsDisplay.textContent = lastKishimenAvgStars;
                    totalTastinessStarsDisplay.classList.add('star-rating');
                    shareButton.disabled = false;
                } else {
                    // Handle error case
                    ramenNameDisplay.textContent = "命名失敗...";
                    aiReviewsContainer.innerHTML = `<p class="text-center text-red-500">AIが感情を爆発させすぎたようです...レビューの取得に失敗しました。</p>`;
                    totalTastinessStarsDisplay.textContent = '評価不能';
                }
            }
    
            function generateResultRamenView() {
                toppingsContainer.innerHTML = ''; ingredientDetailsBody.innerHTML = '';
                const displayableIngredients = caughtIngredients.filter(ing => ing.name !== '爆弾');
                displayableIngredients.forEach((ing, i) => {
                    setTimeout(() => {
                        const topping = document.createElement('div');
                        topping.textContent = ing.emoji; topping.className = 'result-ingredient';
                        const angle = Math.random() * Math.PI * 2; const radius = 20 + Math.random() * 30;
                        topping.style.left = `calc(50% + ${Math.cos(angle) * radius}px)`; topping.style.top = `calc(50% + ${Math.sin(angle) * radius - 20}px)`;
                        topping.style.transform = `translate(-50%, -50%) rotate(${Math.random() * 60 - 30}deg)`;
                        toppingsContainer.appendChild(topping);
                    }, i * 150);
                });
                if (displayableIngredients.length === 0) {
                    ingredientDetailsBody.innerHTML = `<tr><td colspan="4" class="p-4 text-center text-gray-500">食べられる具材がありませんでした...</td></tr>`;
                } else {
                    displayableIngredients.filter(ing => ing.score > 0).forEach(ing => {
                        const row = document.createElement('tr'); row.className = 'bg-white border-b';
                        row.innerHTML = `<td class="px-2 py-2 font-medium text-gray-900 whitespace-nowrap">${ing.emoji} ${ing.name}</td><td class="px-2 py-2 star-rating">${generateStars(Math.round(ing.tastiness / 2))}</td><td class="px-2 py-2">${ing.calories} kcal</td><td class="px-2 py-2 star-rating">${generateStars(ing.recommendation)}</td>`;
                        ingredientDetailsBody.appendChild(row);
                    });
                }
            }
            
            async function fetchAiNameAndReviews(resultData) {
                const ingredientNames = resultData.caughtIngredients.map(ing => ing.name).join(', ') || '具材なし';
                const prompt = `あなたはユーモアのセンスがあるきしめん名付けの達人であり、同時に感情の起伏が激しすぎる評論家です。以下のきしめんデータに基づき、「きしめん名」と3件の「レビュー」を生成してください。
    
                ### きしめんデータ
                - スコア: ${resultData.score}
                - 総カロリー: ${resultData.totalCalories} kcal
                - 使用された具材: ${ingredientNames}
    
                ### 生成ルール
                1.  **きしめん名:**
                    - 創造的で面白く、絶対に人や団体を傷つけない名前を1つ考えてください。
                    - ポジティブで食欲をそそるような名前が望ましいです。
                    - 具材、カロリー、スコアの特徴を捉えてください。
                    - 20文字以内でお願いします。
                2.  **レビュー (3件):**
                    - 3人の異なる客層(例: 「きしめんを愛しすぎた男」「カロリーの悪魔」)になりきってください。
                    - 各レビューは "reviewer_name", "review_text", "stars" (1-5の整数) を含みます。
                    - **感情を爆発させてください。** 良い具材の組み合わせなら天にも昇る気持ちで褒めちぎり、悪いもの(例:長靴)なら怒りや絶望を表現してください。
                    - レビュー(stars)とレビュー内容は完全に一致させてください。
                
                ### 出力形式
                必ず以下のJSON形式で、指定されたプロパティをすべて含んだオブジェクトを返してください。
                `;
    
                const schema = {
                    type: "OBJECT",
                    properties: {
                        kishimen_name: { type: "STRING", description: "創造的で面白いきしめんの名前" },
                        reviews: {
                            type: "ARRAY",
                            description: "3人の評論家によるレビューの配列",
                            items: {
                                type: "OBJECT",
                                properties: {
                                    reviewer_name: { type: "STRING", description: "評論家の名前" },
                                    review_text: { type: "STRING", description: "150文字程度の感情的なレビュー" },
                                    stars: { type: "NUMBER", description: "1から5の星評価" }
                                },
                                required: ["reviewer_name", "review_text", "stars"]
                            }
                        }
                    },
                    required: ["kishimen_name", "reviews"]
                };
    
                try {
                    const payload = {
                        contents: [{ role: "user", parts: [{ text: prompt }] }],
                        generationConfig: { responseMimeType: "application/json", responseSchema: schema }
                    };
                    const apiKey = "";
                    const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-04-17:generateContent?key=${apiKey}`;
                    const response = await fetch(apiUrl, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(payload)
                    });
                    if (!response.ok) throw new Error(`API error: ${response.status}`);
                    const result = await response.json();
    
                    if (result.candidates && result.candidates[0]?.content?.parts[0]?.text) {
                        const parsedResult = JSON.parse(result.candidates[0].content.parts[0].text);
                        // Sanitize the kishimen name
                        parsedResult.kishimen_name = parsedResult.kishimen_name.trim().replace(/['"「」』『]/g, '');
                        return parsedResult;
                    } else {
                        throw new Error("Unexpected API response format.");
                    }
                } catch (error) {
                    console.error("Error calling Gemini API:", error);
                    return null; // Return null on error
                }
            }
    
            // --- イベントリスナー ---
            startButton.addEventListener('click', init);
            restartButton.addEventListener('click', init);
            
            copyPromptButton.addEventListener('click', () => {
                promptTextarea.select();
                document.execCommand('copy');
                copyPromptButton.textContent = 'コピー完了!';
                setTimeout(() => { copyPromptButton.textContent = 'コピー'; }, 2000);
            });
            
            shareButton.addEventListener('click', async () => {
                shareButtonText.textContent = '画像生成中...'; shareButton.disabled = true;
                try {
                    const content = document.getElementById('share-content');
                    const canvas = await html2canvas(content, { backgroundColor: '#ffffff', useCORS: true, scale: 2 });
                    canvas.toBlob(async (blob) => {
                        const file = new File([blob], "kishimen-result.png", { type: "image/png" });
                        const shareText = `私が作った『${lastKishimenName}』!🍲 Geminiの感情評価は「${lastKishimenAvgStars}」だった!\nみんなもGoogleのAI #Gemini  で神ゲーならぬ神きし作ろうぜ! #きしめんキャッチャーゲーム `;
                        if (navigator.canShare && navigator.canShare({ files: [file] })) {
                            await navigator.share({ files: [file], title: 'きしめんキャッチャーゲーム 結果', text: shareText });
                        } else {
                            const link = document.createElement('a'); link.href = URL.createObjectURL(blob);
                            link.download = 'kishimen-result.png'; link.click(); URL.revokeObjectURL(link.href);
                            showNotification("画像シェア非対応のため、画像をダウンロードしました。");
                        }
                    }, 'image/png');
                } catch(err) {
                    console.error('シェア画像の生成に失敗しました:', err);
                    showNotification('シェア画像の生成に失敗しました。', true);
                } finally {
                    shareButtonText.textContent = '結果をシェア'; shareButton.disabled = false;
                }
            });
    
            document.addEventListener('keydown', (e) => {
                if (gameScreen.classList.contains('hidden')) return;
                if (e.key === 'ArrowLeft') player.x = Math.max(0, player.x - player.speed);
                if (e.key === 'ArrowRight') player.x = Math.min(canvas.width - player.width, player.x + player.speed);
            });
    
            let isDragging = false;
            const getEventX = (e) => e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const handleMove = (e) => {
                if (!isDragging || gameScreen.classList.contains('hidden')) return;
                e.preventDefault();
                const rect = canvas.getBoundingClientRect();
                player.x = getEventX(e) - rect.left - player.width / 2;
                player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
            };
            const handleStart = (e) => {
                if (gameScreen.classList.contains('hidden')) return; // Only drag during the game
                isDragging = true; 
                handleMove(e); 
            };
            const handleEnd = () => { isDragging = false; };
    
            canvas.addEventListener('mousedown', handleStart);
            canvas.addEventListener('touchstart', handleStart, { passive: false });
            document.addEventListener('mouseup', handleEnd);
            document.addEventListener('touchend', handleEnd);
            document.addEventListener('mousemove', handleMove);
            document.addEventListener('touchmove', handleMove, { passive: false });
    
            window.addEventListener('resize', () => {
                if (gameScreen.classList.contains('hidden')) return;
                const container = document.getElementById('game-container');
                canvas.width = container.clientWidth > 0 ? container.clientWidth - 20 : 300;
                canvas.height = window.innerHeight * 0.6;
                player.y = canvas.height - player.height - 10;
                drawGame();
            });
        });
        </script>
    </body>
    </html>
    

    App

     
     
    Salsultantのplatypus2000jpです。テック、自然科学(野鳥等)、文化資産の記事がメイン。 ◆DeepResearch:濃密な調査を100円でお得に。 ◆科学で節約:エビデンスに基づく賢いライフハックを。 本質を大切に、読み応えある記事を更新します。

    あなたへのおすすめ