■97個目 「茶虫くんの薬草茶の旅 〜 水たまり大作戦 〜」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、ちょっと不思議で
とっても可愛いキャラクターが主役のゲームを作ってみました。
その名も 「茶虫(ちゃむし)くん」 です!
ぽってりとした緑色の体に、頭には大きな葉っぱの触覚。
そして両手には、湯気がほわほわと立ちのぼる「茶」の文字入りの湯呑み。
おっとりしているけれど、
薬草探しとなると目を輝かせる、
そんな子です。(=v=*)
■ 雨上がりの大ピンチ!
ある晴れた日のこと。
茶虫くんのお散歩コースに、
なんと 巨大な水たまり ができてしまいました。
このままでは、大好きな薬草探しの旅に進めません。
そこで、皆さんの出番です!
■ 葉っぱを浮かべて、道をつくろう
茶虫くんが好きな「薬草の葉っぱ」を水面に浮かべて、
対岸までの道を作ってあげましょう。
登場する葉っぱは全部で12種類。
イタドリ、クワ、ビワ、そしてちょっぴり苦いゴーヤまで、
個性豊かな薬草たちが勢ぞろい。
葉っぱを踏むたびに、茶虫くんは 薬草茶パワー を吸収して元気いっぱい!
特有の「ぬるぬるっ」とした動きで、どんどんスピードアップしていきます。
■ もし落ちちゃっても大丈夫
もし葉っぱの道が途切れて、水たまりにポチャリ……。
そんな時でも安心してください。(^^*)
なんと、#18:”早口実況アメンボレース”のアメンボたちで、
水面をツツーイと滑っていた
アカアシアオアメンボ、アメマアタマアメンボ、
アメアガリアメンボ、アクマアザアメンボ の4匹が、
レースを中断して大急ぎで救出に来てくれます。
誰も傷つかない、やさしい世界での小さな大冒険です。
■ AIとのやりとりで面白かった部分
● ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。
まずはキャラクターから。
薬草茶のキャラクター「茶虫くん」を作りたいです。
青虫っぽい丸い顔で緑色。
黒い点と線の落ち着いた目。
中央に濃い緑の団子鼻、口はちんまり。
頭には柄付き葉っぱの触覚みたいな飾り。
湯呑のお茶を持っていて、ほっぺはほんのり赤色。
この特徴で、図形を組み合わせてキャラを作ってみて。
AI: ゲーム作成、素晴らしいですね!
ご指定の特徴をもとに「茶虫くん」を作成しました。
緑色の丸い顔に点と線の目、葉っぱの触覚、
湯呑みを持った可愛いキャラクターになりました。

(※生成された茶虫くんの画像)
AI: この茶虫くんを使って、どんなゲームにしていきましょう?
次のステップを教えてください。
みー:(^o^/) 茶虫くんの動きはリアルにしたいです。
イモムシの「ぬるっ」とした、
本物みたいで二度見しちゃう動きを再現してほしい。
まずは葉っぱの上を歩かせたいです。
AI: 動画の準備ができました!
……と言いたいところなのですが、
(※ブログづくりのために読み返していたら、
動画の負荷でチャット履歴が全部消える大アクシデント発生!)
~動画自体はXに投稿してあります。
みー:(;_;) AIとのやり取りのパートを仕上げようとしたところで、
履歴が消えちゃって困ってました……。
でも、さっきまで作っていたコードと下書きは
保存してあったので貼り付けますね!
AI: うわあ、バックアップを残していて本当に良かったです!
ナイスリカバリーです。
お預かりしたコード、薬草のセレクトが渋くて最高ですし、
前作「アメンボレース」の4匹が救出隊として登場する
伏線回収には胸が熱くなりました!
みー:(^_^;) よかったです!それでは気を取り直して続きます。
AI: ……あ、それとみーさん。
保存してくれていた操作説明の文章を読んでいたら、
なぜか別のゲームの
「クレーンが自動で動いて素材を投下するルール」が
紛れ込んでましたよ!(笑)
〈あわてて前回の記事のパートも混ぜて貼り付けてました〉
_(め_ゅ)_
■ 実際の動作
まずは完成したスタート画面から。
ここで世界観とゲームの遊び方をまとめて確認できます。

(画像:スタート画面)
茶虫くんは、水たまりの岸から冒険をスタートします。
水面には、なんとレース中のアメンボたちがスイスイと滑走中。
(#18「早口実況アメンボレース」に登場した4匹です!)

(画像:ゲーム画面)
● 葉っぱをドラッグして道をつくろう
プレイヤーは、薬草の葉っぱをドラッグして
茶虫くんの通り道を作っていきます。
薬草茶の上を通ると、茶虫くんは 薬草茶パワー を吸収して元気いっぱい。
ぬるっとした独特の動きで、どんどんスピードアップしていきます。
隙間を作らないように、テンポよく葉っぱを並べていきましょう。
目指すは水たまりの反対側!

(画像:ゲーム画面)
● 落ちてしまったら……
葉っぱをうまくつなげられず、茶虫くんが水たまりにポチャリ……。
その瞬間、ゲームは終了です。

(画像:ゲーム終了とリスタート)
でも大丈夫。
何度でも挑戦できるので、ぜひベストルートを見つけてみてください。
■ コードのポイント
今回の記事では、
AI と相談しながら作った「茶虫くんの薬草茶の旅 〜 水たまり大作戦 〜」 の HTML コードを公開します。
このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。
▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く
Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)
② 下のコードをコピーして貼り付けます。
③ 「chamusi.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。
■ 操作方法
今回のゲームも操作はとっても簡単! マウスだけで遊べます。(^^*)
● 基本操作
- 左クリック & ドラッグ:薬草の葉っぱをつかんで移動
- ドロップ:葉っぱを水面に置く
これだけで OK です。
💡 基本のルール
- 薬草を選ぶ
画面上部の“太陽みたいな薬草トレイ”から、
使いたい葉っぱをクリックします。 - 葉っぱを置く(ドラッグ&ドロップ)
選んだ葉っぱを水面へドラッグして、茶虫くんの通り道を作ります。 - 茶虫くんが進む(自動)
茶虫くんは左から右へ自動で移動します。
葉っぱを踏むと 薬草茶パワー を吸収してスピードアップ! - 道が途切れると……?
葉っぱがつながっていない場所に来ると、
茶虫くんは水たまりにポチャリ。 - 対岸にたどり着けばクリア!
すべての葉っぱを渡りきるとステージクリアです。
ちょっとハラハラする、茶虫くんの小さな大冒険。
ぜひ楽しんでみてくださいね。ヽ(^^*)

(画像:操作説明図)
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>茶虫くんの薬草茶の旅 〜 水たまり大作戦 〜</title><style> * { box-sizing: border-box; user-select: none; } body { margin: 0; padding: 0; background-color: #e3ebd5; font-family: 'Helvetica Neue', Arial, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; color: #2c1810; } #gameContainer { position: relative; box-shadow: 0 10px 30px rgba(0,0,0,0.15); border-radius: 16px; overflow: hidden; background-color: #fff; } canvas { display: block; background-color: #faf6ee; } .ui-overlay { position: absolute; top: 20px; left: 20px; font-size: 18px; font-weight: bold; padding: 10px 20px; border-radius: 30px; pointer-events: none; display: flex; flex-direction: column; gap: 10px; z-index: 5; } .ui-row { display: flex; gap: 20px; } .game-status { color: #2d5a27; text-shadow: 1px 1px 1px rgba(255,255,255,0.8); } .stage-status { color: #e65100; font-size: 24px; text-shadow: 1px 1px 2px rgba(255,255,255,0.9); } .goal-status { color: #1565c0; font-size: 16px; background: rgba(255,255,255,0.7); padding: 4px 10px; border-radius: 10px; } .drag-guide { position: absolute; top: 52px; right: 185px; background: rgba(255, 255, 255, 0.95); color: #d84315; padding: 8px 18px; border-radius: 25px; font-weight: bold; font-size: 16px; border: 2px solid #ff9800; box-shadow: 0 4px 12px rgba(0,0,0,0.15); pointer-events: none; animation: pointRight 1s ease-in-out infinite alternate; z-index: 5; } @keyframes pointRight { 0% { transform: translateX(0); } 100% { transform: translateX(8px); } } #msgScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(45, 90, 39, 0.85); color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px; text-align: center; transition: all 0.3s; z-index: 10; } #msgScreen h2 { font-size: 36px; margin-bottom: 10px; color: #fff3dd; } #msgScreen h3 { color: #ffb74d; margin-top: 0; } #msgScreen p { font-size: 18px; max-width: 600px; line-height: 1.6; margin-bottom: 30px; } .btn { background-color: #ffb74d; color: #2c1810; border: none; padding: 12px 35px; font-size: 20px; font-weight: bold; border-radius: 30px; cursor: pointer; box-shadow: 0 5px 15px rgba(0,0,0,0.2); transition: transform 0.1s; } .btn:hover { background-color: #ffa726; transform: scale(1.05); } .hidden { display: none !important; }</style></head><body><div id="gameContainer"> <canvas id="gameCanvas" width="960" height="540"></canvas> <div class="ui-overlay" id="uiOverlay"> <div class="stage-status">Stage: <span id="stageVal">1</span></div> <div class="ui-row"> <div>スコア: <span id="scoreVal" class="game-status">0</span></div> <div>速度: <span id="speedVal" class="game-status">100%</span></div> <div>次の薬草: <span id="nextHerbVal" style="color:#d84315;">選別中</span></div> </div> <div class="goal-status">目標速度: <span id="goalSpeedVal">300</span>% で対岸へ到達!</div> </div> <div id="dragGuide" class="drag-guide hidden"> 薬草をここからドラッグ! 👉 </div> <!-- スタート / ゲームオーバー / クリア 画面 --> <div id="msgScreen"> <h2 id="screenTitle">茶虫くんの薬草茶の旅</h2> <h3 id="screenSubTitle">〜 水たまり大作戦 〜</h3> <p id="screenDesc"> 画面左からぬるぬると進んでくる「茶虫くん」。目の前には大きな水たまりが広がっています!<br> 上部の薬草トレイ(太陽)から、マウスで薬草の葉をつかんで水面に並べ、道を作ってあげましょう。<br> 茶虫くんが葉っぱを踏むと、薬草茶パワーでどんどんスピードアップします。指定の目標速度に達すると対岸が見えてきます! </p> <button id="startBtn" class="btn">出発する</button> </div></div><script> // ========================================== // ピコピコ 8-bit サウンドエンジン (Web Audio API) // ========================================== const SoundEngine = (function() { let ctx = null; let bgmInterval = null; let isPlaying = false; let tempo = 0.4; const yonanukiScale = [329.63, 392.00, 440.00, 493.88, 587.33, 659.25]; const init = () => { if (!ctx) { ctx = new (window.AudioContext || window.webkitAudioContext)(); } if (ctx.state === 'suspended') ctx.resume(); }; const playTone = (freq, type, startTimeOffset, dur, vol = 0.1) => { if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, ctx.currentTime + startTimeOffset); gain.gain.setValueAtTime(vol, ctx.currentTime + startTimeOffset); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + startTimeOffset + dur); osc.connect(gain); gain.connect(ctx.destination); osc.start(ctx.currentTime + startTimeOffset); osc.stop(ctx.currentTime + startTimeOffset + dur); }; const loopBGM = () => { if (!isPlaying) return; const freq = yonanukiScale[Math.floor(Math.random() * yonanukiScale.length)]; playTone(freq, 'triangle', 0, tempo * 0.8, 0.05); bgmInterval = setTimeout(loopBGM, tempo * 1000); }; return { startBGM: () => { init(); if (!isPlaying) { isPlaying = true; loopBGM(); } }, stopBGM: () => { isPlaying = false; if (bgmInterval) clearTimeout(bgmInterval); }, updateSpeed: (speedRatio) => { tempo = Math.max(0.08, 0.5 / speedRatio); }, seGrab: () => { init(); playTone(600, 'square', 0, 0.05, 0.05); }, sePlace: () => { init(); playTone(300, 'square', 0, 0.1, 0.05); playTone(400, 'square', 0.05, 0.1, 0.05); }, seStep: () => { init(); playTone(880, 'square', 0, 0.05, 0.08); }, seSplash: () => { init(); playTone(150, 'sawtooth', 0, 0.4, 0.15); playTone(100, 'sawtooth', 0.1, 0.4, 0.15); playTone(50, 'sawtooth', 0.2, 0.4, 0.15); }, seClear: () => { init(); [523.25, 659.25, 783.99, 1046.50].forEach((f, i) => { playTone(f, 'square', i * 0.12, 0.3, 0.1); }); } }; })(); const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const uiOverlay = document.getElementById('uiOverlay'); const dragGuide = document.getElementById('dragGuide'); const msgScreen = document.getElementById('msgScreen'); const screenTitle = document.getElementById('screenTitle'); const screenSubTitle = document.getElementById('screenSubTitle'); const screenDesc = document.getElementById('screenDesc'); const startBtn = document.getElementById('startBtn'); const scoreVal = document.getElementById('scoreVal'); const speedVal = document.getElementById('speedVal'); const nextHerbVal = document.getElementById('nextHerbVal'); const stageVal = document.getElementById('stageVal'); const goalSpeedVal = document.getElementById('goalSpeedVal'); uiOverlay.classList.add('hidden'); const HERB_TYPES = [ { name: "イタドリ", color: "#aed581", size: 120, bonus: 0.05 }, { name: "ドクダミ", color: "#33691e", size: 110, bonus: 0.08 }, { name: "クワの葉", color: "#689f38", size: 130, bonus: 0.06 }, { name: "杜仲茶", color: "#8d6e63", size: 115, bonus: 0.09 }, { name: "ビワの葉", color: "#558b2f", size: 150, bonus: 0.07 }, { name: "カキの葉", color: "#9ccc65", size: 140, bonus: 0.06 }, { name: "スギナ", color: "#c5e1a5", size: 110, bonus: 0.10 }, { name: "モリンガ", color: "#7cb342", size: 110, bonus: 0.12 }, { name: "パパイヤ葉", color: "#2e7d32", size: 145, bonus: 0.11 }, { name: "イチョウ葉", color: "#fbc02d", size: 120, bonus: 0.08 }, { name: "石トリソウ", color: "#1b5e20", size: 100, bonus: 0.14 }, { name: "ゴーヤ", color: "#1b5e20", size: 130, bonus: 0.20 } ]; // 新たなステート RESCUING を追加 let gameState = "START"; let score = 0; let totalScore = 0; let currentStage = 1; let baseSpeed = 1.0; let currentSpeed = baseSpeed; let time = 0; let rescueTimer = 0; let leaves = []; let ripples = []; let steams = []; let currentNextHerb = null; let isDragging = false; let mouseX = 0; let mouseY = 0; const trayY = 70; const trayX = 800; const trayRadius = 35; const chamushiX = 250; const chamushiY = 320; const segmentsCount = 7; const segmentSpacing = 22; let bodyPositions = []; let landX = 0; let waterScroll = 0; let targetSpeedPercent = 300; let goalX = -1000; let isGoalVisible = false; let racingGerrids = [ { id: 0, x: 100, y: 135, type: 0, v: 0, glideTimer: 0 }, { id: 1, x: 250, y: 145, type: 1, v: 0, glideTimer: 0 }, { id: 2, x: 400, y: 155, type: 2, v: 0, glideTimer: 0 }, { id: 3, x: 550, y: 165, type: 3, v: 0, glideTimer: 0 }, ]; function drawGerridShape(x, y, type) { ctx.save(); ctx.translate(x, y); ctx.scale(0.8, 0.8); let bodyColor = '#00695c'; if (type === 0) bodyColor = '#00838f'; if (type === 2) { let grad = ctx.createLinearGradient(-15, -10, 15, 10); grad.addColorStop(0, '#e91e63'); grad.addColorStop(0.3, '#3f51b5'); grad.addColorStop(0.6, '#4caf50'); grad.addColorStop(1, '#ffeb3b'); bodyColor = grad; } ctx.fillStyle = bodyColor; ctx.beginPath(); ctx.ellipse(0, 0, 15, 8, 0, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(-5, -5, 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(5, -5, 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(-5, -5, 1, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(5, -5, 1, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#2c1810'; ctx.lineWidth = 1.5; if (type === 0) ctx.strokeStyle = '#c62828'; ctx.beginPath(); ctx.moveTo(-10, 5); ctx.lineTo(-20, 15); ctx.lineTo(-15, 20); ctx.stroke(); ctx.beginPath(); ctx.moveTo(10, 5); ctx.lineTo(20, 15); ctx.lineTo(15, 20); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-5, 8); ctx.lineTo(-15, 25); ctx.lineTo(-10, 30); ctx.stroke(); ctx.beginPath(); ctx.moveTo(5, 8); ctx.lineTo(15, 25); ctx.lineTo(10, 30); ctx.stroke(); if (type === 1) { ctx.fillStyle = '#fbc02d'; ctx.beginPath(); ctx.arc(0, -5, 6, 0, Math.PI * 2); ctx.fill(); } else if (type === 3) { ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(-7, 3, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(7, 3, 2, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } function updateAndDrawRacingGerrids() { for (let i = 0; i < racingGerrids.length; i++) { let g = racingGerrids[i]; if (gameState === "RESCUING" || gameState === "GAMEOVER") { // ★ 救出モード:茶虫くんの周りに集まる let targetX = chamushiX + (i - 1.5) * 40; let targetY = chamushiY + 15; g.rescueX += (targetX - g.rescueX) * 0.1; g.rescueY += (targetY - g.rescueY) * 0.1; drawGerridShape(g.rescueX, g.rescueY, g.type); } else { // 通常のレースモード if (g.glideTimer > 0) { g.glideTimer--; g.v *= 0.98; } else { g.v = 2 + Math.random() * 3; g.glideTimer = 30 + Math.random() * 60; if (g.v < 0.2) g.v = 0.2; } g.x += g.v; let finalX = (g.x + waterScroll) % (canvas.width + 100) - 50; drawGerridShape(finalX, g.y, g.type); } } } function initGame(isFullReset = false) { if (isFullReset) { score = 0; totalScore = 0; currentStage = 1; targetSpeedPercent = 300; } else { score = 0; currentStage++; targetSpeedPercent = 300 + (currentStage - 1) * 10; } currentSpeed = baseSpeed; time = 0; rescueTimer = 0; landX = 0; waterScroll = 0; leaves = []; ripples = []; steams = []; isDragging = false; isGoalVisible = false; goalX = -1000; stageVal.textContent = currentStage; goalSpeedVal.textContent = targetSpeedPercent; scoreVal.textContent = totalScore; speedVal.textContent = "100%"; leaves.push({ x: chamushiX + 60, y: chamushiY, type: HERB_TYPES[2], stepped: false }); leaves.push({ x: chamushiX + 180, y: chamushiY, type: HERB_TYPES[4], stepped: false }); leaves.push({ x: chamushiX + 300, y: chamushiY, type: HERB_TYPES[5], stepped: false }); chooseNextHerb(); bodyPositions = []; for(let i=0; i<segmentsCount; i++) { bodyPositions.push({x: chamushiX - i*segmentSpacing, y: chamushiY}); } } function chooseNextHerb() { const idx = Math.floor(Math.random() * HERB_TYPES.length); currentNextHerb = HERB_TYPES[idx]; nextHerbVal.textContent = currentNextHerb.name; nextHerbVal.style.color = currentNextHerb.color; } startBtn.addEventListener('click', () => { if (gameState === "START" || gameState === "GAMEOVER") { initGame(true); startGame(); } else if (gameState === "CLEAR") { initGame(false); startGame(); } }); function startGame() { gameState = "PLAYING"; msgScreen.classList.add('hidden'); uiOverlay.classList.remove('hidden'); dragGuide.classList.remove('hidden'); SoundEngine.startBGM(); SoundEngine.updateSpeed(currentSpeed / baseSpeed); } function getMousePos(e) { const rect = canvas.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; } canvas.addEventListener('mousedown', (e) => { if (gameState !== "PLAYING") return; const pos = getMousePos(e); const dist = Math.hypot(pos.x - trayX, pos.y - trayY); if (dist < trayRadius + 10) { isDragging = true; mouseX = pos.x; mouseY = pos.y; SoundEngine.seGrab(); } }); canvas.addEventListener('mousemove', (e) => { const pos = getMousePos(e); mouseX = pos.x; mouseY = pos.y; }); canvas.addEventListener('mouseup', (e) => { if (!isDragging) return; isDragging = false; const pos = getMousePos(e); if (pos.y > 140 && pos.x > 0 && pos.x < canvas.width) { leaves.push({ x: pos.x, y: chamushiY, type: currentNextHerb, stepped: false }); ripples.push({x: pos.x, y: chamushiY, r: 10, alpha: 1}); chooseNextHerb(); SoundEngine.sePlace(); } }); function drawBackground() { let skyBarH = 130; let skyGrad = ctx.createLinearGradient(0, 0, 0, skyBarH); skyGrad.addColorStop(0, '#87CEEB'); skyGrad.addColorStop(1, '#E0F7FA'); ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, canvas.width, skyBarH); ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; ctx.beginPath(); ctx.ellipse(120, 60, 45, 28, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(150, 50, 35, 25, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(420, 80, 60, 35, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(450, 70, 40, 30, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(680, 50, 40, 25, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#2d5a27'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(0, skyBarH); ctx.lineTo(canvas.width, skyBarH); ctx.stroke(); let puddleGrad = ctx.createLinearGradient(0, skyBarH, 0, canvas.height); puddleGrad.addColorStop(0, '#4fc3f7'); puddleGrad.addColorStop(0.5, '#29b6f6'); puddleGrad.addColorStop(1, '#0288d1'); ctx.fillStyle = puddleGrad; ctx.fillRect(0, skyBarH, canvas.width, canvas.height - skyBarH); ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 3; for(let i=0; i<15; i++) { let lineX = (i * 80 + waterScroll) % (canvas.width + 100) - 50; let lineY = 160 + (i * 25) % (canvas.height - 160); ctx.beginPath(); ctx.moveTo(lineX, lineY); ctx.lineTo(lineX + 40, lineY); ctx.stroke(); } ctx.strokeStyle = 'rgba(255,255,255,0.4)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, chamushiY); ctx.lineTo(canvas.width, chamushiY); ctx.stroke(); updateAndDrawRacingGerrids(); if (chamushiX + 60 + landX > -200) { ctx.fillStyle = '#8bc34a'; ctx.beginPath(); ctx.moveTo(0, 120); let landEdgeX = Math.max(0, chamushiX + 20 + landX); ctx.lineTo(landEdgeX, 120); ctx.quadraticCurveTo(chamushiX + 60 + landX, chamushiY + 80, chamushiX - 120 + landX, canvas.height); ctx.lineTo(0, canvas.height); ctx.fill(); ctx.strokeStyle = '#558b2f'; ctx.lineWidth = 4; ctx.stroke(); } if (isGoalVisible) { ctx.fillStyle = '#7cb342'; ctx.beginPath(); ctx.moveTo(canvas.width, 120); ctx.lineTo(goalX, 120); ctx.quadraticCurveTo(goalX - 80, chamushiY + 50, goalX + 50, canvas.height); ctx.lineTo(canvas.width, canvas.height); ctx.fill(); ctx.strokeStyle = '#33691e'; ctx.lineWidth = 4; ctx.stroke(); ctx.fillStyle = '#795548'; ctx.fillRect(goalX + 40, 200, 10, 120); ctx.fillStyle = '#ffecb3'; ctx.fillRect(goalX, 150, 120, 60); ctx.strokeStyle = '#5d4037'; ctx.lineWidth = 3; ctx.strokeRect(goalX, 150, 120, 60); ctx.fillStyle = '#e65100'; ctx.font = 'bold 20px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('GOAL!', goalX + 60, 185); } let sunGlow = ctx.createRadialGradient(trayX, trayY, 5, trayX, trayY, trayRadius + 18); sunGlow.addColorStop(0, 'rgba(255, 215, 0, 0.4)'); sunGlow.addColorStop(0.7, 'rgba(255, 165, 0, 0.1)'); sunGlow.addColorStop(1, 'rgba(255, 165, 0, 0)'); ctx.fillStyle = sunGlow; ctx.beginPath(); ctx.arc(trayX, trayY, trayRadius + 18, 0, Math.PI*2); ctx.fill(); let sunGrad = ctx.createRadialGradient(trayX, trayY, 5, trayX, trayY, trayRadius); sunGrad.addColorStop(0, '#FFD700'); sunGrad.addColorStop(1, '#FF8C00'); ctx.fillStyle = sunGrad; ctx.beginPath(); ctx.arc(trayX, trayY, trayRadius, 0, Math.PI*2); ctx.fill(); if (!isDragging && currentNextHerb) { drawLeafShape(trayX, trayY, currentNextHerb, true); } } function drawLeafShape(x, y, herb, isPreview) { ctx.save(); ctx.translate(x, y); ctx.fillStyle = herb.color; const scale = isPreview ? 0.3 : 1.0; ctx.scale(scale, scale); ctx.beginPath(); if (herb.name === "イチョウ葉") { ctx.moveTo(0, 15); ctx.quadraticCurveTo(-35, -20, -40, -40); ctx.quadraticCurveTo(0, -50, 0, -25); ctx.quadraticCurveTo(0, -50, 40, -40); ctx.quadraticCurveTo(35, -20, 0, 15); } else if (herb.name === "ゴーヤ") { ctx.ellipse(0, 0, herb.size, herb.size * 0.5, 0, 0, Math.PI * 2); } else if (herb.name === "パパイヤ葉") { for (let i = 0; i < 7; i++) { let angle = (Math.PI * 2 / 7) * i; ctx.lineTo(Math.cos(angle) * herb.size, Math.sin(angle) * herb.size); ctx.lineTo(Math.cos(angle + 0.4) * (herb.size*0.4), Math.sin(angle + 0.4) * (herb.size*0.4)); } } else { ctx.moveTo(-herb.size, 0); ctx.quadraticCurveTo(0, -herb.size*0.6, herb.size, 0); ctx.quadraticCurveTo(0, herb.size*0.6, -herb.size, 0); } ctx.fill(); ctx.strokeStyle = 'rgba(0,0,0,0.15)'; ctx.lineWidth = isPreview ? 3 : 4; ctx.beginPath(); ctx.moveTo(-herb.size, 0); ctx.lineTo(herb.size, 0); for (let i = -herb.size + 15; i < herb.size; i += 25) { ctx.moveTo(i, 0); ctx.lineTo(i + 15, -herb.size * 0.3); ctx.moveTo(i, 0); ctx.lineTo(i + 15, herb.size * 0.3); } ctx.stroke(); if (!isPreview) { ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(herb.name, 0, 5); } ctx.restore(); } function updateAndDrawRipples() { for (let i = ripples.length - 1; i >= 0; i--) { let r = ripples[i]; r.r += 1.5; r.alpha -= 0.02; if (gameState === "PLAYING" && !isGoalVisible) r.x -= currentSpeed; if (r.alpha <= 0) { ripples.splice(i, 1); continue; } ctx.strokeStyle = `rgba(255,255,255,${r.alpha})`; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(r.x, r.y, r.r, 0, Math.PI*2); ctx.stroke(); } } function createSteam(x, y) { steams.push({ x: x + (Math.random() - 0.5) * 10, y: y, vx: (Math.random() - 0.5) * 0.5, vy: -1 - Math.random() * 1, life: 60 + Math.random() * 30, maxLife: 90 }); } function updateAndDrawSteams() { ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; for (let i = steams.length - 1; i >= 0; i--) { let s = steams[i]; s.x += s.vx; s.y += s.vy; s.life--; if (gameState === "PLAYING" && !isGoalVisible) s.x -= currentSpeed * 0.2; if (s.life <= 0) { steams.splice(i, 1); continue; } let alpha = Math.max(0, (s.life / s.maxLife) * 0.5); ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; ctx.beginPath(); let size = Math.max(1, 10 * (s.life / s.maxLife)); ctx.arc(s.x, s.y, size, 0, Math.PI * 2); ctx.fill(); } } function updateAndDrawChamushi() { if (bodyPositions.length === 0) return; let uNeriSpeed = (gameState === "GOAL_REACHED") ? 0.02 : time * 5; // ★ 救出時は少し水に沈む let sinkOffset = (gameState === "RESCUING" || gameState === "GAMEOVER") ? 15 : 0; for (let i = 0; i < segmentsCount; i++) { const wavePhase = (gameState === "GOAL_REACHED") ? (time*2 - i*1.1) : (uNeriSpeed - i * 1.1); const stretch = Math.sin(wavePhase) * 14; // ★ 救出時は持ち上がらずプカプカするだけ let lift = 0; if (gameState === "RESCUING" || gameState === "GAMEOVER") { lift = Math.sin(time * 5) * 3; } else { lift = Math.max(0, Math.sin(wavePhase)) * -22; } if (gameState === "GOAL_REACHED") { bodyPositions[i].x = chamushiX + ((time - goalReachedTime) * 30) - (i * segmentSpacing) + stretch; } else { bodyPositions[i].x = chamushiX - (i * segmentSpacing) + stretch; } bodyPositions[i].y = chamushiY + lift + sinkOffset; } for (let i = segmentsCount - 1; i > 0; i--) { const pos = bodyPositions[i]; ctx.fillStyle = '#7cb342'; ctx.beginPath(); ctx.arc(pos.x, pos.y, 22, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#558b2f'; ctx.lineWidth = 2; ctx.stroke(); } const head = bodyPositions[0]; const hx = head.x; const hy = head.y - 12; ctx.strokeStyle = '#33691e'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(hx - 10, hy - 18); ctx.quadraticCurveTo(hx - 30, hy - 40, hx - 30, hy - 50); ctx.stroke(); ctx.beginPath(); ctx.moveTo(hx + 10, hy - 18); ctx.quadraticCurveTo(hx + 30, hy - 40, hx + 30, hy - 50); ctx.stroke(); ctx.save(); ctx.translate(hx - 30, hy - 50); ctx.rotate(-Math.PI / 10); ctx.fillStyle = '#1b5e20'; ctx.beginPath(); ctx.ellipse(0, -15, 12, 22, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#33691e'; ctx.lineWidth = 1.5; ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, -40); ctx.stroke(); ctx.restore(); ctx.save(); ctx.translate(hx + 30, hy - 50); ctx.rotate(Math.PI / 10); ctx.fillStyle = '#1b5e20'; ctx.beginPath(); ctx.ellipse(0, -15, 12, 22, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#33691e'; ctx.lineWidth = 1.5; ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, -40); ctx.stroke(); ctx.restore(); ctx.fillStyle = '#7cb342'; ctx.beginPath(); ctx.arc(hx, hy, 34, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#558b2f'; ctx.lineWidth = 2.5; ctx.stroke(); let cheekGrad = ctx.createRadialGradient(hx - 20, hy + 6, 2, hx - 20, hy + 6, 10); cheekGrad.addColorStop(0, 'rgba(255, 90, 90, 0.6)'); cheekGrad.addColorStop(1, 'rgba(255, 90, 90, 0)'); ctx.fillStyle = cheekGrad; ctx.beginPath(); ctx.arc(hx - 20, hy + 6, 10, 0, Math.PI*2); ctx.fill(); let cheekGradR = ctx.createRadialGradient(hx + 20, hy + 6, 2, hx + 20, hy + 6, 10); cheekGradR.addColorStop(0, 'rgba(255, 90, 90, 0.6)'); cheekGradR.addColorStop(1, 'rgba(255, 90, 90, 0)'); ctx.fillStyle = cheekGradR; ctx.beginPath(); ctx.arc(hx + 20, hy + 6, 10, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#222'; ctx.strokeStyle = '#222'; ctx.lineWidth = 2.5; // ★ 救出時の「> <」目 if (gameState === "RESCUING" || gameState === "GAMEOVER") { ctx.beginPath(); ctx.moveTo(hx - 22, hy - 10); ctx.lineTo(hx - 12, hy - 4); ctx.lineTo(hx - 22, hy + 2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(hx + 22, hy - 10); ctx.lineTo(hx + 12, hy - 4); ctx.lineTo(hx + 22, hy + 2); ctx.stroke(); // 汗マーク ctx.fillStyle = 'rgba(100, 200, 255, 0.8)'; ctx.beginPath(); ctx.arc(hx + 25, hy - 15, 4, 0, Math.PI*2); ctx.moveTo(hx + 25 - 4, hy - 15); ctx.lineTo(hx + 25, hy - 25); ctx.lineTo(hx + 25 + 4, hy - 15); ctx.fill(); } else { ctx.beginPath(); ctx.arc(hx - 16, hy - 6, 2.5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.moveTo(hx - 24, hy - 6); ctx.lineTo(hx - 8, hy - 6); ctx.stroke(); ctx.beginPath(); ctx.arc(hx + 16, hy - 6, 2.5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.moveTo(hx + 24, hy - 6); ctx.lineTo(hx + 8, hy - 6); ctx.stroke(); } ctx.fillStyle = '#1b5e20'; ctx.beginPath(); ctx.arc(hx, hy + 4, 11, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.moveTo(hx - 5, hy + 18); ctx.quadraticCurveTo(hx, hy + 21, hx + 5, hy + 18); ctx.stroke(); if (gameState !== "GAMEOVER" && gameState !== "RESCUING" && Math.random() < 0.1) { createSteam(hx, hy + 22); } updateAndDrawSteams(); ctx.fillStyle = '#bcaaa4'; ctx.fillRect(hx - 15, hy + 22, 30, 24); ctx.strokeStyle = '#5d4037'; ctx.lineWidth = 2; ctx.strokeRect(hx - 15, hy + 22, 30, 24); ctx.fillStyle = '#c5e1a5'; ctx.beginPath(); ctx.ellipse(hx, hy + 22, 15, 4, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#2e7d32'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('茶', hx, hy + 39); ctx.fillStyle = '#7cb342'; ctx.beginPath(); ctx.arc(hx - 16, hy + 32, 6, 0, Math.PI * 2); ctx.arc(hx + 16, hy + 32, 6, 0, Math.PI * 2); ctx.fill(); } let goalReachedTime = 0; function updateGameLogic() { // ★ 救出モード中はここでタイマーを進めて終了させる if (gameState === "RESCUING") { rescueTimer++; if (rescueTimer > 150) { // 約2.5秒後にゲームオーバー画面へ gameState = "GAMEOVER"; showGameOver(); } return; // 画面スクロールや当たり判定はスキップ } let currentPercent = Math.floor((currentSpeed / baseSpeed) * 100); if (currentPercent >= targetSpeedPercent && !isGoalVisible) { isGoalVisible = true; goalX = canvas.width; } if (isGoalVisible) { goalX -= currentSpeed; landX -= currentSpeed; waterScroll -= currentSpeed * 0.5; for (let i = 0; i < leaves.length; i++) { leaves[i].x -= currentSpeed; } if (goalX <= chamushiX + 60) { gameState = "GOAL_REACHED"; goalReachedTime = time; SoundEngine.stopBGM(); SoundEngine.seClear(); setTimeout(showClearScreen, 1500); } } else { landX -= currentSpeed; waterScroll -= currentSpeed * 0.5; for (let i = 0; i < leaves.length; i++) { leaves[i].x -= currentSpeed; } } if (leaves.length > 0 && leaves[0].x < -150) { leaves.shift(); } const headX = bodyPositions[0].x; let onAnyLeaf = false; if (headX < chamushiX + 20 + landX) { onAnyLeaf = true; } if (isGoalVisible && headX > goalX - 50) { onAnyLeaf = true; } for (let i = 0; i < leaves.length; i++) { let leaf = leaves[i]; if (headX >= leaf.x - leaf.type.size && headX <= leaf.x + leaf.type.size) { onAnyLeaf = true; if (!leaf.stepped && gameState === "PLAYING") { leaf.stepped = true; score += 10; totalScore += 10; currentSpeed += leaf.type.bonus; scoreVal.textContent = totalScore; speedVal.textContent = Math.floor((currentSpeed / baseSpeed) * 100) + "%"; SoundEngine.seStep(); SoundEngine.updateSpeed(currentSpeed / baseSpeed); } } } // ★ 水に落ちた時の処理を RESCUING に変更 if (!onAnyLeaf && gameState === "PLAYING") { gameState = "RESCUING"; rescueTimer = 0; SoundEngine.stopBGM(); SoundEngine.seSplash(); // アメンボたちの救出目標位置をセット for (let i = 0; i < racingGerrids.length; i++) { let g = racingGerrids[i]; g.rescueX = (g.x + waterScroll) % (canvas.width + 100) - 50; if (g.rescueX < -50) g.rescueX += (canvas.width + 100); g.rescueY = g.y; } } } function showGameOver() { uiOverlay.classList.add('hidden'); dragGuide.classList.add('hidden'); msgScreen.classList.remove('hidden'); screenTitle.textContent = "アメンボたちのやさしい救出!"; screenSubTitle.textContent = "ああっ!水たまりにポチャリ..."; screenDesc.innerHTML = `薬草の道が途切れてしまいましたが、レース中のアメンボたちが<br>急いで駆けつけて助けてくれました。<br><strong style="font-size:24px; color:#ffb74d;">通算スコア: ${totalScore}点</strong><br>到達速度: ${Math.floor((currentSpeed / baseSpeed) * 100)}% / 目標 ${targetSpeedPercent}%`; startBtn.textContent = "お礼を言ってやり直す"; } function showClearScreen() { gameState = "CLEAR"; uiOverlay.classList.add('hidden'); dragGuide.classList.add('hidden'); msgScreen.classList.remove('hidden'); screenTitle.textContent = "ステージクリア!"; screenSubTitle.textContent = `Stage ${currentStage} 突破!`; let nextTarget = 300 + (currentStage) * 10; screenDesc.innerHTML = `素晴らしい!茶虫くんは無事に対岸へたどり着きました。<br>薬草茶パワーも全開です!<br><br><strong style="font-size:24px; color:#ffb74d;">現在の通算スコア: ${totalScore}点</strong><br><br>次のステージ(Stage ${currentStage + 1})は、目標速度 <strong style="color:#aed581;">${nextTarget}%</strong> で対岸が見えてきます。`; startBtn.textContent = "次のステージへ進む"; } function loop() { ctx.clearRect(0, 0, canvas.width, canvas.height); drawBackground(); for (let i = 0; i < leaves.length; i++) { drawLeafShape(leaves[i].x, leaves[i].y, leaves[i].type, false); } updateAndDrawRipples(); updateAndDrawChamushi(); if (gameState === "PLAYING" || gameState === "RESCUING") { updateGameLogic(); time += 0.02 + (gameState === "RESCUING" ? 0 : (currentSpeed * 0.003)); } else if (gameState === "GOAL_REACHED") { time += 0.02; } else if (gameState === "START" || gameState === "GAMEOVER" || gameState === "CLEAR") { time += 0.02; } if (isDragging && currentNextHerb) { drawLeafShape(mouseX, mouseY, currentNextHerb, false); ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(mouseX, mouseY); ctx.lineTo(mouseX, chamushiY); ctx.stroke(); ctx.setLineDash([]); } requestAnimationFrame(loop); } initGame(true); loop();</script></body></html>
■ 今日の学び
今回は、いつも通りキャラクターデザインから始まったゲーム制作でした。
私の熱意が伝わったのか、AI側もいつになく気合いが入っていて、
普段は見せないようなキャラクター画像やアニメーション動画を
次々と生成してくれました。
……が、ここからが怒涛の展開でした。₍Ꙭ₎
ブログ記事を仕上げるために、
「さあ、AIとのやり取りを読み返そう」と
最初の動画付近までスクロールした、その瞬間。
画面がピタッと固まり、
次の瞬間には—— そこから先の大切なやり取りが、
跡形もなく消えていたのです。
まるで夢から覚めたみたいに、ログがすべて真っ白。
あの時の心臓のヒュッとする感じは、今思い出しても忘れられません。
幸いにも、完成したゲームコードだけは別で保存していたため、
作品そのものが消える最悪の事態は免れました。
でも、AIと一緒に試行錯誤した“対話の足跡”が消えてしまったショックは、
正直かなり大きかったです。(>x<、)
今回の件で得た最大の学びは、
「AIとの開発では、予期せぬトラブルでデータが消えることも普通に起こり得る」 という、とてもリアルな教訓でした。
原因を一つに断定することはできませんが、
画像や動画の処理負荷、セッションの限界など、
いろんな要素が重なったのだと思います。
大切にしていた記録が消えたのは確かに悲しい出来事でした。
でも、この経験も“成長の一歩”だと信じています。
私の座右の銘は、とてもシンプルに「進歩」。
どんな壁にぶつかっても、それをバネにして昨日より今日、
今日より明日へと進み続けること。
それこそが、この「100のゲーム実験室」を動かす原動力なんだと、
改めて実感しました。
今回のアクシデントと、そこからの奇跡の復元劇も含めて、
97個目のゲームは忘れられない作品になりました!
読んでくれたあなたにも、
少しでもこの冒険のドキドキが伝わっていたら嬉しいです。
■ 次回は、手作り新聞バッグから生まれるゲームです。
新聞で作った手作りバッグって、おしゃれですよね。
記事の文字や写真、イラストがそのまま模様になって、
世界にひとつだけの“紙のアート作品”みたいな存在感があります。
同じ新聞でも、折り方や切り取り方で雰囲気がガラッと変わる。
その偶然性もまた、手作り新聞バッグの魅力です。
そんな新聞バッグをテーマにした、 ちょっとユニークで、
ちょっとアートなゲームを作ります。
次回もお楽しみに!ヽ(*^v^*)ノ
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)





🌈はじめての方へ
🌳 実験一覧
🌱 このブログについて
🍃 プライバシーポリシー