■50個目「スイカ転がし」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
記念すべき50個目のゲームは、ハラハラドキドキのアクションパズル!
今回転がすのは……なんと “とってもデリケートなスイカ” です。
画面をそーっと傾けながら、迷路のようなコースをスイカが割れないように運んでいく、緊張感たっぷりのゲームになっています。
今回こだわったのは、スイカの「究極のみずみずしさ」と「音」。(=v=*)
「壁にコツンと当たっただけでも傷んじゃうくらい繊細なスイカを、
イノシシの突進から守り抜いてゴールできたら……最高に美味しいそう!」
そんなちょっと過激な遊び心から生まれた作品です。
ステージは全部で5つ。コースの形もどんどん変化していきます。
① 基本の迷路コース
② イノシシが上下から突撃してくるスピードステージ
③ 外枠なし!落ちたら終わりの天空コース
④ 穴だらけの極細道コース
⑤ 最終ステージは……プレイしてのお楽しみ!
スイカは壁に強く擦れるだけでもダメージを受けてしまうので、思わず息を止めてしまうほどの緊張感。 さらに、猛スピードで突撃してくるイノシシが迷路の壁をドカン!と壊していく、予測不能なハプニングも……!
そして、すべての困難を乗り越えて全5ステージをクリアすると、 とびきり美味しそうな「真紅のカットスイカ」のご褒美が待っています。🍉✨
クリックするたびに「シャクッ!」という最高にみずみずしい音が鳴るので、 ぜひ何度でも味わってみてくださいね。(^▽^*)
■ AIとのやりとりで面白かった部分
■ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。html形式で動かせるゲーム。
スイカ転がしゲーム。通路を微調整しながら転がして、
ゴールまでスイカを運ぶゲーム。丁寧に扱わないとスイカが割れます。
AI: 「スイカ転がしゲーム」、そのざっくりした提案だけでもワクワクしてくるね!
通路を調整するパズル要素と、「丁寧に扱わないと割れちゃう」というハラハラ感の組み合わせは、プレイヤーを夢中にさせそうだ。
みー:(^o^)ノ 床を傾ける方式のゲームで、時々イノシシが画面を横断するようにしたい。 当たらないようにスピード調整しながらゴールを目指す感じ。
AI: 「床傾け方式」の繊細な操作感に、予測不能な「突撃イノシシ」を組み合わせるなんて、すごく面白いアイデアだよ!
ゆっくり行きたいけど、イノシシが来るから急がなきゃ……
でも急ぐと壁にぶつかって割れちゃう。このジレンマが最高だね。
■豆知識:猪とスイカ
実は、現実のスイカ農家にとっても猪は大敵。 猪は鼻がとても良くて、
熟したスイカの匂いをすぐ嗅ぎつけるんだ。
しかも皮を器用に剥いで、中身だけ綺麗に食べてしまうこともあるらしい。
ゲームの中で猪がスイカに突進してくるのも、あながち間違いじゃないんだね。
みー:(^w^)* 横画面になっているのを、上からの視点でできるかな。 シンプル迷路で、スタートからゴールまでスイカを割らずに転がすゲーム。 イノシシは迷路の形に関係なく横断して、通った後は地形が変わります。
AI: さらにワクワクするアイデアだね! トップダウン視点の迷路に、
壁をなぎ倒して進む「地形破壊イノシシ」。
猪が通った後に新しいルートができたり、
逆に危険地帯になったり……状況判断が必要なスリル満点のゲームになりそうだ。
■豆知識:猪突猛進
猪は興奮すると時速40km以上で突進することがある。
その破壊力は木の柵や薄い金属フェンスをなぎ倒すほど。
ゲームの中で壁を壊して進むのも、実はリアルな表現なんだよ。
みー:(^o^)/* 画面自体を動かして、上下左右に傾けている感じを出せるかな。
イノシシが壊したところから画面外に落ちたらゲームオーバーにしましょう。
AI: そのアイデア、最高にスリリングだね!
画面がぐりぐり動いて箱を傾けているような感覚。
さらに、猪が開けた穴から落ちる危険まで追加されて、緊張感が一気にアップするよ。
☕ マスターの豆知識:スイカが勝手に爆発する!?
よく熟したスイカは内部の水分と糖分がパンパンで、常に外側へ向けて強い圧力がかかっている。
そのため、少しの衝撃や温度変化で「ボーン!」と弾けるように割れてしまうことがある。 農業用語では「裂果(れっか)」と呼ばれる現象なんだ。
みーさんが「強く当たると割れるようにしたい」と言ったのは、
実はスイカのリアルな物理法則にぴったりだったというわけ。
■ 実際の動作
最初にAIと相談して作ってもらった初期バージョンです。
この頃は、ただ坂を転がるだけのシンプルなゲームでした。

(画像:初期のゲーム画面)
こちらが仕上がりのゲーム画面です。
スイカの世界観に全振りしています。o(* ゜v ゜*)o”

(画像:スタート画面)
左上からスタートして、
マウスで画面を傾けながらスイカを転がし、ゴール地点を目指します。
茶色いのがイノシシで、地形を破壊しながら突進してきます。

(画像:ゲーム画面)
全5ステージ構成で、ゴールに入ると次のステージへ進めます。

(画像:ステージクリア)
耐久度が0になるとスイカがはじけてゲームオーバー。
外に落ちてもゲームオーバー。

(画像:ゲームオーバー)
クリアすると、AIにお願いして作ってもらった
最高にみずみずしいスイカが登場します。
さらに、リアリティを高めるためにAIに作ってもらった
スイカの咀嚼音「シャクッ!」も楽しめます。

(画像:ご褒美スイカ)
■ コードのポイント
今回の記事では、AI と相談しながら作った「スイカ転がし」の HTML コードを公開します。
このコードをそのまま保存するだけで、あなたのPCでもすぐに遊べるようになります。
ブラウザだけで動く、とてもシンプルなゲームなので、気軽に試してみてくださいね。
▼ 遊ぶまでの手順
1. PC の「メモ帳」(※Windowsの場合)や、
Mac の「テキストエディット」など、テキストエディタを開きます。
2. 下のコードをコピーして貼り付けます。
3. 「suikakorogasi.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
4. 保存したファイルをブラウザで開けば、すぐに遊べます。
■ 遊び方・操作説明 🍉
🎮 マウスを動かすだけで遊べます!
ゲームがスタートしたら、
画面の上でマウスをゆっくり動かしてみてください。(^_^)
マウスの位置に合わせて、ゲームの「床」全体がぐらぐらと傾きます。
本物の箱を両手で持って傾けるようなイメージで、
そーっとスイカを転がしてあげてくださいね。
💔 ぶつかり&こすれ注意!
このスイカ、とってもデリケートです。(><)
- 壁に「ゴツッ!」とぶつかる
- 壁に沿って「ガリガリッ」とこすりながら進む
こんな動きをすると画面が赤く光り、「耐久度(HP)」が減ってしまいます。
耐久度が 0 になると……パカーン!と粉砕されてゲームオーバー。
なるべく道の真ん中をキープして、優しく転がすのがコツですよ♪
🐗 突撃イノシシにご用心!
のんびり転がしていると、突然「猪(イノシシ)」が猛スピードで突進してきます!
イノシシは迷路の壁をなぎ倒して、
そのまま「ガレキ(通れる道)」に変えてしまうほどパワフル。
スイカが跳ね飛ばされると大ダメージを受けてしまうので、
- 通り過ぎるのを待つ
- 逆に急いで逃げ切る
など、状況に合わせて判断してくださいね。(=v=*)
🕳️ 奈落への落下に注意!
ステージが進むと、外枠の壁がない「フチなしステージ」も登場します。
うっかり外に飛び出すと、そのまま奈落の底へ真っ逆さま……。
さらに、イノシシが外枠を壊して作った「穴」から落ちることもあるので、
油断大敵です!
🧺 めざせ出荷(ゴール)!
画面の中にある黄色の「ゴールエリア(出荷カゴ)」までスイカを運べばステージクリア!(ノ´▽`)ノ クリア時の残り耐久度が高いほど、たくさんスコアがもらえます。
全5ステージの「スイカワールドツアー」。
あなたはこの繊細なスイカを無事に出荷できるでしょうか?
すべてクリアした人だけが味わえる、 「シャクッ!」という最高のご褒美をぜひ楽しんでくださいね。(^^*)
※ここから先はコードになります。必要な方だけ開いてください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>🍉スイカ転がし・ワールドツアー🍉</title> <style> /* 【追加】背景にうっすらと🍉絵文字を敷き詰める */ body { margin: 0; overflow: hidden; background-color: #0d1117; /* 深い背景色 */ background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCI+PHRleHQgeD0iMTAiIHk9IjMwIiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMDUpIiBmb250LXNpemU9IjI0Ij7wn4zsPC90ZXh0Pjwvc3ZnPg=='); background-repeat: repeat; font-family: 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; perspective: 1200px; color: #fff; } #gameContainer { position: relative; transform-style: preserve-3d; } canvas { border: 8px solid #8d6e63; border-radius: 12px; box-shadow: 0 30px 60px rgba(0,0,0,0.8); background-color: #eee; } .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.9); z-index: 100; transform: translateZ(50px); text-align: center; padding: 20px; box-sizing: border-box; border-radius: 12px;} .hidden { display: none !important; } h1 { font-size: 48px; margin: 0 0 10px; text-shadow: 0 4px 8px rgba(0,0,0,0.5); color: #fff; } h2 { font-size: 36px; margin: 0 0 20px; color: #ffc107; text-shadow: 0 0 10px #ffc107; } p { font-size: 24px; margin: 0 0 30px; font-weight: bold; } button { font-size: 28px; font-weight: bold; padding: 15px 50px; cursor: pointer; border-radius: 15px; background-color: #d32f2f; color: white; border: 4px solid #1b5e20; box-shadow: 0 5px 0 #1b5e20; transition: all 0.1s; margin: 10px; } button:hover { background-color: #e53935; } button:active { box-shadow: 0 2px 0 #1b5e20; transform: translateY(3px); } #ui { position: absolute; top: 20px; left: 20px; pointer-events: none; color: #333; font-size: 18px; font-weight: bold; text-shadow: 1px 1px 0 #fff; background-color: rgba(255, 255, 255, 0.8); padding: 8px; border-radius: 8px; transform: translateZ(30px); border: 2px solid #1b5e20; } #damageFlash { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 0, 0, 0); pointer-events: none; transition: background-color 0.1s; transform: translateZ(60px); border-radius: 12px; } .ranking-list { list-style: none; padding: 0; font-size: 22px; text-align: left; background: rgba(255,255,255,0.1); padding: 15px; border-radius: 10px; width: 280px; margin: 0 auto 20px; border: 1px solid rgba(255,255,255,0.3); } .ranking-list li { margin-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,0.3); padding-bottom: 4px; display: flex; justify-content: space-between; color: #fff; } /* 【追加】クリア画面の🍉デコレーション */ .deco-emoji { font-size: 60px; margin: 10px; } .clear-title-row { display: flex; align-items: center; justify-content: center; width: 100%; } #allClearScreen { justify-content: start; padding-top: 20px; } #resultCanvas { margin: 10px 0; border: none; background: transparent; box-shadow: none; cursor: pointer; transition: transform 0.05s ease-in-out; } #resultCanvas:active { transform: scale(0.95); } </style></head><body><div id="gameContainer"> <canvas id="gameCanvas" width="800" height="600"></canvas> <div id="damageFlash"></div> <div id="ui" class="hidden"> 🍉 STAGE: <span id="stageDisp">1</span>/5<br> ❤️ 耐久度: <span id="durability">100</span>%<br> 🏆 TOTAL: <span id="scoreDisp">0</span> </div></div><div id="startScreen" class="overlay"> <div class="clear-title-row"> <span class="deco-emoji">🍉</span> <h1>スイカ転がし<br>ワールドツアー</h1> <span class="deco-emoji">🍉</span> </div> <p>全5ステージ!耐久度を残してハイスコアを目指せ!<br>※音が出ます。マウスで盤面を傾けてね。</p> <button onclick="startGame()">スタート!🍉</button></div><div id="stageClearScreen" class="overlay hidden"> <div class="clear-title-row"> <span class="deco-emoji">✨</span> <h2>STAGE CLEAR!</h2> <span class="deco-emoji">✨</span> </div> <div style="font-size: 50px; margin-bottom: 20px;">🍉🏃</div> <p id="clearText">耐久度 100% × ステージボーナス!</p> <button onclick="startNextStage()">次の🍉ステージへ</button></div><div id="gameOverScreen" class="overlay hidden"> <div class="clear-title-row"> <span class="deco-emoji">💀</span> <h1 style="color: #f44336;">GAME OVER</h1> <span class="deco-emoji">💀</span> </div> <div style="font-size: 60px; margin-bottom: 20px;">💥🍉</div> <p id="reasonText">スイカが割れちゃいました...</p> <button onclick="returnToTitle()">タイトルへ戻る</button></div><div id="allClearScreen" class="overlay hidden"> <div class="clear-title-row"> <span class="deco-emoji">🎊</span> <h1 style="color: #4caf50;">ALL CLEAR!!!</h1> <span class="deco-emoji">🎊</span> </div> <p>完璧なスイカ職人だ!ご褒美をどうぞ!(クリックで食べる)</p> <div style="display:flex; align-items:center; justify-content:center;"> <span class="deco-emoji">🌿</span> <canvas id="resultCanvas" width="400" height="300"></canvas> <span class="deco-emoji">🌿</span> </div> <h2>最終スコア: <span id="finalScore">0</span></h2> <ul class="ranking-list" id="rankingBoard"></ul> <button onclick="returnToTitle()">タイトルへ戻る</button></div><script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const gameContainer = document.getElementById('gameContainer'); const ui = document.getElementById('ui'); const uiDurability = document.getElementById('durability'); const uiStage = document.getElementById('stageDisp'); const uiScore = document.getElementById('scoreDisp'); const resultCanvas = document.getElementById('resultCanvas'); const screens = { start: document.getElementById('startScreen'), clear: document.getElementById('stageClearScreen'), over: document.getElementById('gameOverScreen'), allClear: document.getElementById('allClearScreen') }; let gameState = 'TITLE'; let currentStage = 1; const maxStages = 5; let totalScore = 0, durability = 100, isSmashed = false; let targetTiltX = 0, targetTiltY = 0, currentTiltX = 0, currentTiltY = 0; let nextBoarTime = 0; const friction = 0.98, tiltFactor = 0.25, boarDamage = 60; const cellSize = 50; const cols = canvas.width / cellSize, rows = canvas.height / cellSize; let maze = []; const watermelon = { x: 0, y: 0, vx: 0, vy: 0, radius: 18 }; // 【追加】confettiに種類を持たせる let smashParticles = [], confetti = [], boars = []; let startCell = { col: 1, row: 1 }; let goalCell = { col: cols - 2, row: rows - 2 }; let goalArea = { x: 0, y: 0, radius: 35 }; let audioCtx, rollOsc, rollGain, isAudioInitialized = false; function initAudio() { if(isAudioInitialized) return; const AudioContext = window.AudioContext || window.webkitAudioContext; audioCtx = new AudioContext(); rollOsc = audioCtx.createOscillator(); rollGain = audioCtx.createGain(); rollOsc.type = 'triangle'; rollOsc.frequency.value = 30; rollGain.gain.value = 0; rollOsc.connect(rollGain); rollGain.connect(audioCtx.destination); rollOsc.start(); isAudioInitialized = true; } function playSound(type, val) { if(!audioCtx) return; const currentTime = audioCtx.currentTime; const osc = audioCtx.createOscillator(), gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); if(type === 'crash') { osc.type = 'square'; osc.frequency.setValueAtTime(150 + Math.random()*50, currentTime);osc.frequency.exponentialRampToValueAtTime(40, currentTime + 0.1);gain.gain.setValueAtTime(Math.min(0.8, val * 0.1), currentTime);gain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.1);osc.start(); osc.stop(currentTime + 0.1); } else if(type === 'goal') { osc.type = 'sine'; osc.frequency.value = val;gain.gain.setValueAtTime(0, currentTime);gain.gain.linearRampToValueAtTime(0.3, currentTime + 0.05);gain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.5);osc.start(); osc.stop(currentTime + 0.5); } else if(type === 'bite') { const bufferSize = audioCtx.sampleRate * 0.1; const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); const data = buffer.getChannelData(0);for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; const noiseSource = audioCtx.createBufferSource(); noiseSource.buffer = buffer; const noiseFilter = audioCtx.createBiquadFilter(); noiseFilter.type = 'highpass'; noiseFilter.frequency.setValueAtTime(3000, currentTime); const noiseGain = audioCtx.createGain(); noiseGain.gain.setValueAtTime(0, currentTime); noiseGain.gain.linearRampToValueAtTime(0.3, currentTime + 0.005); noiseGain.gain.exponentialRampToValueAtTime(0.001, currentTime + 0.05); noiseSource.connect(noiseFilter); noiseFilter.connect(noiseGain); noiseGain.connect(audioCtx.destination); noiseSource.start(currentTime); noiseSource.stop(currentTime + 0.05); const oscBody = audioCtx.createOscillator(); oscBody.type = 'sine'; oscBody.frequency.setValueAtTime(150, currentTime); oscBody.frequency.exponentialRampToValueAtTime(80, currentTime + 0.03); const gainBody = audioCtx.createGain(); gainBody.gain.setValueAtTime(0, currentTime); gainBody.gain.linearRampToValueAtTime(0.5, currentTime + 0.005); gainBody.gain.exponentialRampToValueAtTime(0.001, currentTime + 0.03); oscBody.connect(gainBody); gainBody.connect(audioCtx.destination); oscBody.start(currentTime); oscBody.stop(currentTime + 0.03); } } document.addEventListener('mousemove', (e) => { if(gameState !== 'PLAYING' || isSmashed) return; const rect = document.body.getBoundingClientRect(); targetTiltX = ((e.clientX - rect.left) - window.innerWidth / 2) / (window.innerWidth / 2); targetTiltY = ((e.clientY - rect.top) - window.innerHeight / 2) / (window.innerHeight / 2); }); resultCanvas.addEventListener('click', () => { if (gameState === 'CLEAR' && currentStage >= maxStages) { if(audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); playSound('bite'); } }); function buildStage(stageNum) { maze = []; for (let r = 0; r < rows; r++) { maze[r] = []; for (let c = 0; c < cols; c++) { let isWall = false; if (stageNum === 1) { if (r===0 || r===rows-1 || c===0 || c===cols-1 || (r%3===0 && c>2 && c<cols-3) || (c%4===0 && r>2 && r<rows-3 && Math.random()<0.5)) isWall = true; } else if (stageNum === 2) { if (r===0 || r===rows-1 || c===0 || c===cols-1 || (r%2===0 && c>1 && c<cols-2 && Math.random()<0.8)) isWall = true; } else if (stageNum === 3) { if (r===0 || r===rows-1 || c===0 || c===cols-1) isWall = false; else if ((r%3===0 && c%3!==0) || (c%4===0 && r%2!==0)) isWall = true; } else if (stageNum === 4) { if (r===0 || r===rows-1 || c===0 || c===cols-1 || (r%2===0 && c%2===0)) isWall = true; } else if (stageNum === 5) { if (r===0 || r===rows-1 || c===0 || c===cols-1) isWall = false; else if (Math.random() < 0.3) isWall = true; } maze[r][c] = isWall ? 1 : 0; } } startCell = { col: 1, row: 1 }; goalCell = { col: cols - 2, row: rows - 2 }; if(stageNum === 3 || stageNum === 5) { startCell = { col: 2, row: 2 }; goalCell = { col: cols - 3, row: rows - 3 }; } for(let i=0; i<=1; i++) for(let j=0; j<=1; j++) { maze[startCell.row+i][startCell.col+j] = 0; maze[goalCell.row-i][goalCell.col-j] = 0; } goalArea = { x: (goalCell.col + 0.5) * cellSize, y: (goalCell.row + 0.5) * cellSize, radius: 35 }; } function spawnBoar() { let isVert = false; if (currentStage === 2) isVert = true; else if (currentStage >= 4) isVert = Math.random() < 0.5; const speed = (6 + currentStage) + Math.random() * 4; if (isVert) { const side = Math.random() < 0.5 ? 'top' : 'bottom'; const bCol = 1 + Math.floor(Math.random() * (cols - 2)); boars.push({ x: (bCol + 0.5) * cellSize - 25, y: side==='top'?-100:canvas.height+100, vx: 0, vy: side==='top'?speed:-speed, width: 50, height: 80, isVert: true }); } else { const side = Math.random() < 0.5 ? 'left' : 'right'; const bRow = 1 + Math.floor(Math.random() * (rows - 2)); boars.push({ x: side==='left'?-100:canvas.width+100, y: (bRow + 0.5) * cellSize - 25, vx: side==='left'?speed:-speed, vy: 0, width: 80, height: 50, isVert: false }); } } function showScreen(screenId) { Object.values(screens).forEach(s => s.classList.add('hidden')); if(screenId) screens[screenId].classList.remove('hidden'); } function startGame() { initAudio(); totalScore = 0; currentStage = 1; ui.classList.remove('hidden'); startLevel(); } function startLevel() { if(audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); gameState = 'PLAYING'; isSmashed = false; durability = 100; boars = []; smashParticles = []; confetti = []; buildStage(currentStage); watermelon.x = (startCell.col + 0.5) * cellSize; watermelon.y = (startCell.row + 0.5) * cellSize; watermelon.vx = 0; watermelon.vy = 0; currentTiltX = 0; currentTiltY = 0; targetTiltX = 0; targetTiltY = 0; gameContainer.style.transform = `rotateX(0deg) rotateY(0deg)`; uiStage.textContent = currentStage; uiScore.textContent = totalScore; uiDurability.textContent = durability; showScreen(null); nextBoarTime = Date.now() + 2000; } function startNextStage() { currentStage++; startLevel(); } function returnToTitle() { gameState = 'TITLE'; ui.classList.add('hidden'); showScreen('start'); } // 【修正】真っ赤で美味しいスイカ(修正済み) function drawResultWatermelon() { const resCtx = resultCanvas.getContext('2d'); const w = resultCanvas.width, h = resultCanvas.height; resCtx.clearRect(0,0,w,h); resCtx.save(); resCtx.translate(w/2, h/2 + 30); const radius = 180, angleW = 60 * Math.PI / 180; const startA = (180 - 30) * Math.PI / 180, endA = (180 + 30) * Math.PI / 180; // 皮(緑) resCtx.fillStyle = '#2e7d32'; resCtx.beginPath(); resCtx.moveTo(0, 0); resCtx.arc(0, 0, radius, startA, endA); resCtx.closePath(); resCtx.fill(); const sliceC = 8; resCtx.fillStyle = '#1b5e20'; for(let i=0; i<sliceC; i++) { if(i%2===1) { const a1 = startA + (angleW * i / sliceC), a2 = startA + (angleW * (i+1) / sliceC); resCtx.beginPath(); resCtx.moveTo(0, 0); resCtx.arc(0, 0, radius, a1, a2); resCtx.closePath(); resCtx.fill(); } } // 果皮(白) resCtx.fillStyle = '#f1f8e9'; resCtx.beginPath(); resCtx.moveTo(0, 0); resCtx.arc(0, 0, radius * 0.95, startA, endA); resCtx.closePath(); resCtx.fill(); // 果肉(真紅) const gradFlesh = resCtx.createRadialGradient(0, 0, 0, 0, 0, radius * 0.90); gradFlesh.addColorStop(0, '#ff5252'); gradFlesh.addColorStop(1, '#c62828'); resCtx.fillStyle = gradFlesh; resCtx.beginPath(); resCtx.moveTo(0, 0); resCtx.arc(0, 0, radius * 0.90, startA, endA); resCtx.closePath(); resCtx.fill(); // 繊維テクスチャ resCtx.strokeStyle = 'rgba(211, 47, 47, 0.4)'; resCtx.lineWidth = 1.5; for(let i=0; i<40; i++) { const a = startA + Math.random() * angleW; const rStart = radius * 0.1 + Math.random() * radius * 0.4; resCtx.beginPath(); resCtx.moveTo(rStart * Math.cos(a), rStart * Math.sin(a)); resCtx.lineTo(radius * 0.88 * Math.cos(a), radius * 0.88 * Math.sin(a)); resCtx.stroke(); } // 種 resCtx.fillStyle = '#212121'; for (let i=0; i<18; i++) { const r = radius * 0.3 + Math.random() * radius * 0.5, a = startA + Math.random() * angleW; const sX = r * Math.cos(a), sY = r * Math.sin(a), sSize = 4 + Math.random() * 2; resCtx.save(); resCtx.translate(sX, sY); resCtx.rotate(a + Math.PI/2); resCtx.beginPath(); resCtx.ellipse(0, 0, sSize/2, sSize, 0, 0, Math.PI*2); resCtx.fill(); resCtx.restore(); } // ハイライト resCtx.fillStyle = 'rgba(255, 255, 255, 0.5)'; for(let i=0; i<25; i++) { const r = radius * 0.1 + Math.random() * radius * 0.8, a = startA + Math.random() * angleW; resCtx.beginPath(); resCtx.arc(r*Math.cos(a), r*Math.sin(a), 2 + Math.random()*3, 0, Math.PI*2); resCtx.fill(); } resCtx.strokeStyle = 'rgba(255, 255, 255, 0.6)'; resCtx.lineWidth = 3; resCtx.beginPath(); resCtx.arc(0, 0, radius * 0.7, startA + 0.1, startA + 0.3); resCtx.stroke(); resCtx.restore(); } function triggerSmash(reason) { isSmashed = true; durability = 0; uiDurability.textContent = 0; playSound('crash', 20); for(let i=0; i<40; i++) { const angle = Math.random() * Math.PI * 2, speed = Math.random() * 15 + 5; smashParticles.push({ x: watermelon.x, y: watermelon.y, vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed, size: Math.random()*8+3, type: Math.random() }); } setTimeout(() => { gameState = 'OVER'; document.getElementById('reasonText').textContent = reason; showScreen('over'); }, 1500); } // 【修正】ゴールパーティクルを🍉まみれに! function triggerGoal() { gameState = 'CLEAR'; const stageScore = durability * currentStage * 10; totalScore += stageScore; uiScore.textContent = totalScore; if (currentStage >= maxStages) { playSound('bite'); drawResultWatermelon(); } else { [440, 554.37, 659.25, 880].forEach((f, i) => setTimeout(()=>playSound('goal', f), i*150)); } // スイカカラー+絵文字のパーティクルを生成 const colors = ['#d32f2f', '#1b5e20', '#f1f8e9', '#212121', '#FFD700']; const emojis = ['🍉', '🍉', '🍉', '▫️', '💧']; // 🍉多め、▫️は種、💧は果汁 for(let i=0; i<120; i++) { confetti.push({ x: goalArea.x+(Math.random()-0.5)*40, y: goalArea.y+(Math.random()-0.5)*40, vx: (Math.random()-0.5)*25, vy: (Math.random()-0.5)*25-5, size: Math.random()*10+5, color: colors[Math.floor(Math.random()*colors.length)], emoji: (Math.random() < 0.4) ? emojis[Math.floor(Math.random()*emojis.length)] : null, // 40%で絵文字 rot: Math.random()*360, rotSpeed: (Math.random()-0.5)*20 }); } setTimeout(() => { if (currentStage >= maxStages) { saveRanking(totalScore); document.getElementById('finalScore').textContent = totalScore; showRanking(); showScreen('allClear'); } else { document.getElementById('clearText').innerHTML = `❤️残り耐久度 ${durability}%<br>🏆スコア +${stageScore}`; showScreen('clear'); } }, 2000); } function saveRanking(score) { let ranks = JSON.parse(localStorage.getItem('watermelonRanks')) || []; ranks.push({ score: score }); ranks.sort((a,b) => b.score - a.score); ranks = ranks.slice(0, 5); localStorage.setItem('watermelonRanks', JSON.stringify(ranks)); } function showRanking() { const ranks = JSON.parse(localStorage.getItem('watermelonRanks')) || []; document.getElementById('rankingBoard').innerHTML = ranks.map((r, i) => `<li><span>${i+1}位</span> <span>${r.score} pt</span></li>`).join(''); } function gameLoop() { if(gameState === 'PLAYING') { currentTiltX += (targetTiltX - currentTiltX) * 0.1; currentTiltY += (targetTiltY - currentTiltY) * 0.1; gameContainer.style.transform = `rotateX(${-currentTiltY * 20}deg) rotateY(${currentTiltX * 20}deg)`; if(isAudioInitialized && !isSmashed) { const speed = Math.sqrt(watermelon.vx*watermelon.vx + watermelon.vy*watermelon.vy); rollGain.gain.setTargetAtTime(Math.min(0.3, speed*0.03), audioCtx.currentTime, 0.1); rollOsc.frequency.setTargetAtTime(30 + speed*5, audioCtx.currentTime, 0.1); } if(!isSmashed) { watermelon.vx += currentTiltX * tiltFactor; watermelon.vy += currentTiltY * tiltFactor; watermelon.vx *= friction; watermelon.vy *= friction; checkWallCollision(watermelon.x + watermelon.vx, watermelon.y + watermelon.vy); watermelon.x += watermelon.vx; watermelon.y += watermelon.vy; if(watermelon.x < -watermelon.radius || watermelon.x > canvas.width + watermelon.radius || watermelon.y < -watermelon.radius || watermelon.y > canvas.height + watermelon.radius) triggerSmash("奈落の底へ落下した..."); boars.forEach(boar => { if (watermelon.x+watermelon.radius > boar.x && watermelon.x-watermelon.radius < boar.x+boar.width && watermelon.y+watermelon.radius > boar.y && watermelon.y-watermelon.radius < boar.y+boar.height) { durability -= boarDamage; document.getElementById('damageFlash').style.backgroundColor = 'rgba(255,0,0,0.4)'; setTimeout(()=>document.getElementById('damageFlash').style.backgroundColor = 'transparent', 100); boars = boars.filter(b => b !== boar); if(durability <= 0) triggerSmash("猪のタックルで粉砕されました!"); } }); const dx = watermelon.x - goalArea.x, dy = watermelon.y - goalArea.y; if(Math.sqrt(dx*dx + dy*dy) < goalArea.radius - watermelon.radius) triggerGoal(); } boars.forEach(boar => { boar.x += boar.vx; boar.y += boar.vy; const c = Math.floor((boar.x+boar.width/2)/cellSize), r = Math.floor((boar.y+boar.height/2)/cellSize); if(c>=0 && c<cols && r>=0 && r<rows && maze[r][c]===1) maze[r][c] = 2; }); boars = boars.filter(b => b.x>-150 && b.x<canvas.width+150 && b.y>-150 && b.y<canvas.height+150); if(!isSmashed && Date.now() > nextBoarTime) { spawnBoar(); nextBoarTime = Date.now() + Math.max(1000, 3500 - (currentStage * 400)) + Math.random()*2000; } } if(gameState !== 'TITLE') { ctx.clearRect(0, 0, canvas.width, canvas.height); if(currentStage === 3 || currentStage === 5) { ctx.fillStyle = '#222'; ctx.fillRect(0,0,canvas.width,canvas.height); } for(let r=0; r<rows; r++){ for(let c=0; c<cols; c++){ const ct = maze[r][c]; if(ct === 1) { ctx.fillStyle = '#5d4037'; ctx.fillRect(c*cellSize, r*cellSize, cellSize, cellSize); } else if(ct === 2) { ctx.fillStyle = '#bcaaa4'; ctx.fillRect(c*cellSize, r*cellSize, cellSize, cellSize); ctx.fillStyle = 'rgba(0,0,0,0.1)'; ctx.fillRect(c*cellSize+5, r*cellSize+5, cellSize-10, cellSize-10); } else if(ct === 0 && (currentStage===3 || currentStage===5)) { ctx.fillStyle = '#eee'; ctx.fillRect(c*cellSize, r*cellSize, cellSize, cellSize); } } } ctx.fillStyle = 'rgba(129, 212, 250, 0.5)'; ctx.fillRect(startCell.col*cellSize, startCell.row*cellSize, cellSize, cellSize); ctx.fillStyle = 'rgba(255, 238, 88, 0.5)'; ctx.fillRect(goalCell.col*cellSize, goalCell.row*cellSize, cellSize, cellSize); ctx.beginPath(); ctx.arc(goalArea.x, goalArea.y, goalArea.radius, 0, Math.PI*2); ctx.strokeStyle = '#fbc02d'; ctx.lineWidth = 6; ctx.stroke(); ctx.fillStyle = '#333'; ctx.font = '26px serif'; ctx.fillText('🧺', goalArea.x-13, goalArea.y+8); ctx.fillStyle = '#795548'; boars.forEach(b => { ctx.fillRect(b.x, b.y, b.width, b.height); ctx.fillStyle = '#fff'; if(b.isVert) { if(b.vy > 0) { ctx.fillRect(b.x+10, b.y+b.height-10, 5, 15); ctx.fillRect(b.x+b.width-15, b.y+b.height-10, 5, 15); } else { ctx.fillRect(b.x+10, b.y-5, 5, 15); ctx.fillRect(b.x+b.width-15, b.y-5, 5, 15); } } else { if(b.vx > 0) { ctx.fillRect(b.x+b.width-15, b.y+b.height-15, 20, 7); } else { ctx.fillRect(b.x-5, b.y+b.height-15, 20, 7); } } ctx.fillStyle = '#795548'; }); if (isSmashed) { smashParticles.forEach(p => { p.x+=p.vx; p.y+=p.vy; p.vx*=0.92; p.vy*=0.92; ctx.fillStyle = p.type<0.6 ? '#e53935' : (p.type<0.8 ? '#2e7d32' : '#212121'); ctx.beginPath(); if(p.type>0.8) ctx.ellipse(p.x, p.y, p.size/2, p.size, 0, 0, Math.PI*2); else ctx.arc(p.x, p.y, p.size, 0, Math.PI*2); ctx.fill(); }); } else if (gameState === 'PLAYING' || gameState === 'CLEAR') { ctx.beginPath(); ctx.arc(watermelon.x-3, watermelon.y+3, watermelon.radius, 0, Math.PI*2); ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.fill(); ctx.beginPath(); ctx.arc(watermelon.x, watermelon.y, watermelon.radius, 0, Math.PI*2); ctx.fillStyle = '#4caf50'; ctx.fill(); ctx.strokeStyle = '#1b5e20'; ctx.lineWidth = 2; ctx.stroke(); ctx.beginPath(); ctx.arc(watermelon.x, watermelon.y, watermelon.radius*0.7, 0, Math.PI*2); ctx.strokeStyle = '#1b5e20'; ctx.lineWidth = 3; ctx.stroke(); ctx.beginPath(); ctx.arc(watermelon.x, watermelon.y, watermelon.radius*0.4, 0, Math.PI*2); ctx.stroke(); } // 【修正】紙吹雪描画(絵文字対応) if (gameState === 'CLEAR') { confetti.forEach(p => { p.x+=p.vx; p.y+=p.vy; p.vy+=0.5; p.rot+=p.rotSpeed; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot*Math.PI/180); if(p.emoji) { ctx.font = `${p.size * 2}px serif`; // サイズ調整 ctx.fillText(p.emoji, -p.size, p.size/2); } else { ctx.fillStyle = p.color; ctx.fillRect(-p.size/2, -p.size/2, p.size, p.size*0.6); } ctx.restore(); }); } } requestAnimationFrame(gameLoop); } function checkWallCollision(nx, ny) { const r = watermelon.radius, cS = Math.floor((nx-r)/cellSize), cE = Math.ceil((nx+r)/cellSize), rS = Math.floor((ny-r)/cellSize), rE = Math.ceil((ny+r)/cellSize); let hit = false, speed = 0; for(let ri=rS; ri<rE; ri++){ for(let ci=cS; ci<cE; ci++){ if(ri>=0 && ri<rows && ci>=0 && ci<cols && maze[ri][ci]===1) { const wX=ci*cellSize, wY=ri*cellSize, cX=Math.max(wX, Math.min(nx, wX+cellSize)), cY=Math.max(wY, Math.min(ny, wY+cellSize)); const dx=nx-cX, dy=ny-cY; if(dx*dx+dy*dy < r*r) { hit = true; speed = Math.sqrt(watermelon.vx*watermelon.vx + watermelon.vy*watermelon.vy); const oX=r-Math.abs(dx), oY=r-Math.abs(dy); if(oX>oY) { watermelon.vy*=-0.5; watermelon.y+=(dy>0?oY:-oY); } else { watermelon.vx*=-0.5; watermelon.x+=(dx>0?oX:-oX); } } } } } if(hit && speed>0.2) { const dmg = Math.max(1, Math.floor(speed*speed*0.8)); durability -= dmg; uiDurability.textContent = durability; playSound('crash', dmg); document.getElementById('damageFlash').style.backgroundColor = 'rgba(255,0,0,0.4)'; setTimeout(()=>document.getElementById('damageFlash').style.backgroundColor = 'transparent', 100); if(durability<=0 && !isSmashed) triggerSmash(dmg<=3 ? "擦れすぎて限界を迎えました..." : "壁に強打して粉砕されました!"); } } gameLoop();</script></body></html>
■ 今日の学び
今回のゲームは、好きな食べ物は?と聞かれたら
迷わず「スイカ!」と答えてしまうほど大好きなスイカをテーマに作ってみました。
#15(スイカ割りゲーム)でもスイカ好きを語っていましたが、
毎年スイカの季節が来るのを本当に楽しみにしています。
そして今回登場した「突撃猪」は、
#2(丸太郎と突撃猪)にも登場しているキャラクター!
シリーズの中で、少しずつ世界がつながっていくのも楽しいところです。
スイカの大きく丸いフォルム、つややかな濃い緑の表面に黒い縞模様。
中はきれいな赤色と黒い種が並んでいて、
表皮に向かってのグラデーションも美しい。
甘さとみずみずしさ、そして「シャクッ」としたさわやかな食感……。
想像するだけでも幸せです(= v、=*)~🍉
スイカに全振りしたゲーム、ぜひ楽しんでみてください。(*^o^)。”
■ 次回は、スライス、乾燥のシミュレーターです。
シミュレーターゲームも記事にしていないのがたまってきたので、
まずはスライサーと乾燥機のシミュレーターから紹介していきます。
シミュレーターゲームも体験してもらえると嬉しいです。
次回もお楽しみに。(*^v^)ノシ
▶ 次の実験はこちら(爽快なスライスと乾燥)
「スライス乾燥シミュレーター」
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

コメントを残す