■21個目「ミミズの耕し」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、「ミミズの耕し」の制作実験です。
どんなゲームか一言でいうと、動き続けるミミズを操作して、
土壌を耕していくゲームです。
土壌環境が整うことで、植物は生き生きと育ちます。
きれいな花や、おいしそうな実、生き生きとした葉っぱ…。
見えているところだけではなくて、
地面の中にもたくさんの生き物がいて、
植物が根を張って…と。
それぞれが影響しあっていることに気づかされるゲームです。(つv=)🌱🌳
■ AIとのやりとりで面白かった部分
(^^*)html形式のメモアプリで動かせるゲームを今日も作ります。
ミミズが主人公で、土の中を自動で前進します。
操作できるのは進行方向で、キーボードの矢印キーで操作します。
ミミズが通った土の場所は色が変わります。
ミミズは土の中だけ存在でき、頭が土の外や画面外へ出るとゲームオーバー。
自分の体に当たってもゲームオーバーです。
土の中にはミミズのご飯が10個ほど隠れていて、
食べるごとに体が一つ長くなります。
土の中を9割通り終えるとクリア。
ミミズのおかげで土壌が改良され、地上の花が満開になります。
※お邪魔要素として、時々モグラが横断していきます。
右からか左からかはランダム。
ミミズと同じスピードで通り過ぎ、頭が当たると食べられてゲームオーバー。
ミミズは最初5マス分くらいの大きさ。
ステージの広さは横30・縦20くらい。
地上部分は15くらいの高さにします。
(AI)いらっしゃいませ!常連さん、今日もHTML5の1ファイルで完結するゲームですね。
ミミズが主役の土壌改良アクション、
通称「アースワーム・ガーデナー」を制作しました。
地中のミミズ(緑)を操作して、土を耕しながら(色を変えながら)
エサを探し、花を咲かせるゲームです。
モグラにはくれぐれも気をつけて。
(AI)ミミズに関する豆知識
ミミズは「地球の耕運機」と呼ばれます。
彼らが土を食べることで土壌に隙間ができ、酸素が行き渡るようになります。
さらに、ミミズの排泄物は窒素やリンが豊富で、
最高の肥料になるんですよ。
まさにこのゲームのように、彼らが動くほど花は元気に育ちます。
(*^o^)モグラの大きさは4倍ぐらいにしよう。リプレイ機能もつけよう。
(AI)常連さん、さらに手ごわい仕様へのアップデートですね!承知いたしました。
モグラのサイズを4倍(2×2マス分)に巨大化し、判定を強化しました。
さらに、ゲームオーバーやクリア後に、
耕した軌跡を最初から振り返るリプレイ機能を実装しています。
〈ここで“リトライ”を“リプレイ”と間違えていたことに気づきます〉
(==;)リプレイ再生機能はなくしましょう。
(AI)承知いたしました!
リプレイ機能を外し、シンプルに「現在のレベルをやり直す」か
「最初から」を選べる構成に整理しました。
リプレイ用の記録処理を削除したことでコードも軽くなり、
純粋にアクションゲームとして集中して遊べるようになっています。
〈少しの伝えミスで、大きく影響するみたいです〉(・_・`)
■ 実際の動作
最初のアイデア相談のときに、AI が作ってくれた
「ミミズの耕し」ゲームです。
(土の表面近くの点がモグラです。)

今回の仕上がりです。
雲も動いていて、のどかな風景になりました。
モグラの通ったあとも、しっかり耕されます。

白い丸い球はアイテムで、取ると仲間が増えます。

増えた仲間はピンク色で、自由に動いて耕してくれます。
モグラに食べられるといなくなります。

90%以上を耕すとクリアで、地上部に花が咲きます。

作成途中の相談で AI が作ってくれた画像①です。
(*^^)「地上部が寂しいので、植物や風景を背景的に表示できないかな。」と相談。

作成途中の相談で AI が作ってくれた画像②です。 ゲームの雰囲気がイメージどおりです(*^^*) (*^^)「自然の中のミミズな感じで。ゲームの雰囲気にも合うと思うんだ。」と相談。

■ コードのポイント
今回の記事では、AI と相談しながら作った
「ミミズの耕し」の HTML コードを公開します。
ブラウザだけで動く、とてもシンプルなゲームです。
PC の「メモ帳」(※Windowsの場合)を開き、
下のコードをコピーして貼り付け、「mimizu.html」など好きな名前で保存してください。
保存したファイルをブラウザで開くだけで遊べます。
【操作方法】
・難易度を選びます。
・矢印キーでミミズの向きを操作します。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>ミミズの耕し</title> <style> body { background: #1a1a1a; display: flex; flex-direction: column; align-items: center; font-family: sans-serif; color: #f5f5f5; margin: 0; padding: 10px; overflow: hidden; } canvas { border: 6px solid #3e2723; background: #5d4037; box-shadow: 0 0 30px rgba(0,0,0,0.7); } .ui { margin-bottom: 8px; text-align: center; } .btn { background: #689f38; border: none; color: white; padding: 10px 20px; font-size: 0.9rem; cursor: pointer; border-radius: 20px; margin: 4px; box-shadow: 0 4px 0 #33691e; transition: 0.2s; } .btn:hover { background: #7cb342; transform: scale(1.05); } .btn.retry { background: #ef6c00; box-shadow: 0 4px 0 #e65100; } .btn.lvl { background: #546e7a; width: 80px; box-shadow: 0 4px 0 #263238; } #status { font-weight: bold; color: #cddc39; margin: 5px; height: 1.2em; text-shadow: 1px 1px 2px black; } #startScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to bottom, #81d4fa, #2c1e1a); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 20; } .grid-select { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-top: 15px; } </style></head><body> <div id="startScreen"> <h1>~ミミズの耕し~</h1> <p>レベルを選んで開始!クリアで花を咲かせよう</p> <div class="grid-select"> <button class="btn lvl" onclick="startGame(1)">Lv. 1</button> <button class="btn lvl" onclick="startGame(6)">Lv. 6</button> <button class="btn lvl" onclick="startGame(11)">Lv. 11</button> <button class="btn lvl" onclick="startGame(16)">Lv. 16</button> <button class="btn lvl" onclick="startGame(21)">Lv. 21</button> <button class="btn lvl" onclick="startGame(26)">Lv. 26</button> <button class="btn lvl" onclick="startGame(31)">Lv. 31</button> <button class="btn lvl" onclick="startGame(36)">Lv. 36</button> <button class="btn lvl" onclick="startGame(41)">Lv. 41</button> <button class="btn lvl" onclick="startGame(46)">Lv. 46</button> </div> </div> <div class="ui"> <div style="font-size: 1.1rem;">LEVEL: <span id="levelDisplay">1</span> | 改良率: <span id="progress">0</span>%</div> <div id="status">土を耕して花を咲かせましょう</div> <button id="nextBtn" class="btn" onclick="nextLevel()" style="display:none;">次のレベルへ</button> <button id="retryBtn" class="btn retry" onclick="init()" style="display:none;">リトライ</button> <button id="backBtn" class="btn" onclick="location.reload()" style="display:none;">レベル選択へ</button> </div> <canvas id="gameCanvas"></canvas><script>const cvs = document.getElementById("gameCanvas");const ctx = cvs.getContext("2d");const progressText = document.getElementById("progress");const levelDisplay = document.getElementById("levelDisplay");const statusText = document.getElementById("status");const nextBtn = document.getElementById("nextBtn");const retryBtn = document.getElementById("retryBtn");const backBtn = document.getElementById("backBtn");const startScreen = document.getElementById("startScreen");const SCALE = 20;const WIDTH = 30;const HEIGHT = 35; const SOIL_START = 15;cvs.width = WIDTH * SCALE;cvs.height = HEIGHT * SCALE;let worm, buddies = [], dir, nextDir, foods, moles, grid, gameState, particles = [], level = 1, clouds = [];let itemBall = null, itemTimer = 0;for(let i=0; i<5; i++) clouds.push({x: Math.random()*cvs.width, y: Math.random()*100 + 20, s: Math.random()*0.3 + 0.1});function startGame(selectedLevel) { level = selectedLevel; startScreen.style.display = "none"; init(); requestAnimationFrame(loop);}function init() { worm = [{x: 15, y: 25}, {x: 15, y: 26}, {x: 15, y: 27}, {x: 15, y: 28}, {x: 15, y: 29}]; buddies = []; dir = {x: 0, y: -1}; nextDir = {x: 0, y: -1}; gameState = 0; particles = []; moles = []; itemBall = null; itemTimer = 0; levelDisplay.innerText = level; grid = []; for(let y=0; y<HEIGHT; y++) { grid[y] = []; for(let x=0; x<WIDTH; x++) grid[y][x] = (y >= SOIL_START ? 0 : -1); } grid[25][15] = 1; foods = []; while(foods.length < 10) { let fx = Math.floor(Math.random() * WIDTH), fy = Math.floor(Math.random() * (HEIGHT - SOIL_START)) + SOIL_START; if (!foods.some(f => f.x === fx && f.y === fy)) foods.push({x: fx, y: fy}); } nextBtn.style.display = "none"; retryBtn.style.display = "none"; backBtn.style.display = "none"; statusText.innerText = "モグラに注意して耕しましょう"; const moleCount = level < 6 ? 1 : (level < 15 ? 2 : 3); for(let i=0; i < moleCount; i++) { setTimeout(() => { if(gameState === 0) spawnMole(i); }, 1000 + (i * 2000)); }}function spawnMole(id) { const speed = 0.35 + (level * 0.06); const m = { active: true, y: Math.floor(Math.random() * (HEIGHT - SOIL_START - 2)) + SOIL_START, x: Math.random() > 0.5 ? -3 : WIDTH + 1, dx: 0, id: id }; m.dx = m.x < 0 ? speed : -speed; moles[id] = m;}function createEffect(x, y) { for(let i=0; i<8; i++) { particles.push({ x: x * SCALE + 10, y: y * SCALE + 10, vx: (Math.random() - 0.5) * 5, vy: (Math.random() - 0.5) * 5, life: 1.0, color: "#ffff00" }); }}function drawBackground() { ctx.fillStyle = "#81d4fa"; ctx.fillRect(0, 0, cvs.width, SOIL_START * SCALE); ctx.fillStyle = "white"; clouds.forEach(c => { c.x += c.s; if(c.x > cvs.width + 50) c.x = -60; ctx.beginPath(); ctx.arc(c.x, c.y, 15, 0, Math.PI*2); ctx.arc(c.x+12, c.y-8, 12, 0, Math.PI*2); ctx.arc(c.x+24, c.y, 15, 0, Math.PI*2); ctx.fill(); }); ctx.fillStyle = "#90a4ae"; [[60, 100], [180, 130], [480, 90]].forEach(m => { ctx.beginPath(); ctx.moveTo(m[0]-m[1], SOIL_START*SCALE); ctx.lineTo(m[0], SOIL_START*SCALE-m[1]); ctx.lineTo(m[0]+m[1], SOIL_START*SCALE); ctx.fill(); }); ctx.fillStyle = "#8bc34a"; ctx.fillRect(0, (SOIL_START - 1) * SCALE, cvs.width, SCALE);}function drawNatureFlowers() { for(let i=0; i<WIDTH; i++) { let x = i * SCALE + 10, y = (SOIL_START - 1) * SCALE; let isBloom = (gameState === 2); ctx.fillStyle = isBloom ? `hsl(${i*40}, 70%, 60%)` : "#689f38"; ctx.fillRect(x-1, y-8, 2, 8); // 茎 ctx.beginPath(); ctx.arc(x, y-10, isBloom ? 6 : 3, 0, Math.PI*2); ctx.fill(); if(isBloom) { ctx.fillStyle = "yellow"; ctx.beginPath(); ctx.arc(x, y-10, 2, 0, Math.PI*2); ctx.fill(); } }}window.addEventListener("keydown", e => { if (gameState !== 0) return; if (e.key === "ArrowUp" && dir.y === 0) nextDir = {x: 0, y: -1}; if (e.key === "ArrowDown" && dir.y === 0) nextDir = {x: 0, y: 1}; if (e.key === "ArrowLeft" && dir.x === 0) nextDir = {x: -1, y: 0}; if (e.key === "ArrowRight" && dir.x === 0) nextDir = {x: 1, y: 0};});let lastTime = 0;function loop(timestamp) { if (timestamp - lastTime > 160) { update(); lastTime = timestamp; } draw(); requestAnimationFrame(loop);}function update() { if (gameState !== 0) return; itemTimer++; if (itemTimer % 62 === 0) { if (itemBall) itemBall = null; else itemBall = { x: Math.floor(Math.random()*WIDTH), y: Math.floor(Math.random()*(HEIGHT-SOIL_START))+SOIL_START }; } dir = nextDir; const head = {x: worm[0].x + dir.x, y: worm[0].y + dir.y}; if (head.x < 0 || head.x >= WIDTH || head.y < SOIL_START || head.y >= HEIGHT || worm.some(s => s.x === head.x && s.y === head.y)) { endGame(1); return; } if (itemBall && head.x === itemBall.x && head.y === itemBall.y) { itemBall = null; const bX = (head.x + 2) % WIDTH; buddies.push({ pos: [{x: bX, y: head.y}], d: {x: 1, y: 0} }); statusText.innerText = "仲間が現れた!"; } worm.unshift(head); grid[head.y][head.x] = 1; const fIdx = foods.findIndex(f => f.x === head.x && f.y === head.y); if (fIdx !== -1) { foods.splice(fIdx, 1); createEffect(head.x, head.y); // エフェクト復活 } else { worm.pop(); } // 仲間移動とエサ判定 buddies.forEach((b, bi) => { if (Math.random() < 0.3) { const dirs = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}]; b.d = dirs[Math.floor(Math.random()*4)]; } let bHead = {x: b.pos[0].x + b.d.x, y: b.pos[0].y + b.d.y}; if (bHead.x < 0 || bHead.x >= WIDTH || bHead.y < SOIL_START || bHead.y >= HEIGHT) { b.d = {x: -b.d.x, y: -b.d.y}; bHead = {x: b.pos[0].x + b.d.x, y: b.pos[0].y + b.d.y}; } b.pos.unshift(bHead); if (b.pos.length > 5) b.pos.pop(); grid[bHead.y][bHead.x] = 1; // 仲間がエサを食べた場合もエフェクト const bfIdx = foods.findIndex(f => f.x === bHead.x && f.y === bHead.y); if (bfIdx !== -1) { foods.splice(bfIdx, 1); createEffect(bHead.x, bHead.y); } }); moles.forEach(m => { if (m.active) { m.x += m.dx; let mx = Math.floor(m.x), my = m.y; if (head.x >= mx && head.x < mx+2 && head.y >= my && head.y < my+2) endGame(1); buddies.forEach((b, bi) => { if (b.pos[0].x >= mx && b.pos[0].x < mx+2 && b.pos[0].y >= my && b.pos[0].y < my+2) { buddies.splice(bi, 1); statusText.innerText = "仲間が身代わりになった..."; } }); for(let ox=0; ox<2; ox++) for(let oy=0; oy<2; oy++) { if(mx+ox >= 0 && mx+ox < WIDTH && my+oy >= SOIL_START && my+oy < HEIGHT) grid[my+oy][mx+ox] = 1; } if (m.x < -5 || m.x > WIDTH + 3) { m.active = false; setTimeout(() => spawnMole(m.id), 2000); } } }); // エフェクト更新 particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.life -= 0.1; }); particles = particles.filter(p => p.life > 0); const progress = Math.floor((grid.flat().filter(c => c === 1).length / (WIDTH * (HEIGHT - SOIL_START))) * 100); progressText.innerText = progress; if (progress >= 90) endGame(2);}function draw() { ctx.clearRect(0, 0, cvs.width, cvs.height); drawBackground(); drawNatureFlowers(); // 花の描画 for (let y = SOIL_START; y < HEIGHT; y++) { for (let x = 0; x < WIDTH; x++) { ctx.fillStyle = grid[y][x] === 1 ? "#3e2723" : "#6d4c41"; ctx.fillRect(x * SCALE, y * SCALE, SCALE - 0.5, SCALE - 0.5); } } if (itemBall) { ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(itemBall.x*SCALE+10, itemBall.y*SCALE+10, 8, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = "#81d4fa"; ctx.lineWidth = 2; ctx.stroke(); } // 仲間 buddies.forEach(b => { b.pos.forEach((seg, i) => { ctx.fillStyle = i === 0 ? "#ec407a" : "#f48fb1"; ctx.beginPath(); ctx.arc(seg.x*SCALE+10, seg.y*SCALE+10, 8, 0, Math.PI*2); ctx.fill(); }); }); // プレイヤー worm.forEach((seg, i) => { ctx.fillStyle = i === 0 ? "#4caf50" : "#81c784"; ctx.beginPath(); ctx.arc(seg.x*SCALE+10, seg.y*SCALE+10, 9, 0, Math.PI*2); ctx.fill(); if(i===0) { ctx.fillStyle="black"; ctx.beginPath(); ctx.arc(seg.x*SCALE+7, seg.y*SCALE+8, 1.5, 0, Math.PI*2); ctx.arc(seg.x*SCALE+13, seg.y*SCALE+8, 1.5, 0, Math.PI*2); ctx.fill(); } }); // モグラ moles.forEach(m => { if(m.active) { const mx = m.x * SCALE, my = m.y * SCALE; ctx.fillStyle = "#4e342e"; ctx.beginPath(); ctx.ellipse(mx+20, my+20, 18, 14, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = "#ffab91"; ctx.beginPath(); ctx.arc(m.dx > 0 ? mx+32 : mx+8, my+22, 5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = "black"; ctx.beginPath(); ctx.arc(m.dx > 0 ? mx+25 : mx+15, my+15, 2, 0, Math.PI*2); ctx.fill(); } }); // キラキラエフェクトの描画復活 particles.forEach(p => { ctx.globalAlpha = p.life; ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, Math.PI*2); ctx.fill(); }); ctx.globalAlpha = 1.0;}function endGame(type) { gameState = type; statusText.innerText = type === 1 ? "モグラに邪魔されました..." : "大成功!地上の花が満開です!"; if (type === 1) retryBtn.style.display = "inline-block"; else nextBtn.style.display = "inline-block"; backBtn.style.display = "inline-block";}function nextLevel() { level++; init(); }</script></body></html>
■ 今日の学び
今回のゲームの主人公はミミズでした。
ミミズもこうしてゲームにしてみると、
だんだんキャラクターに愛着がわいてきます。
仲間を増やすアイテムは、ミミズの卵をイメージしています。
最近は、ゲームのイメージに合わせた伝え方を意識するようになりました。
最初の設定を慎重に伝えることで、
AIが作るゲームの方向性がより安定する気がします。
とはいえ、AIが最初に出してくれるゲームは、いつも完ぺきではありません。
だからこそ、“実験”という視点でAIと調整していく過程が面白いと感じています。
(*^_^*)
思いどおりにいかない部分も含めて、共同作業の楽しさがあります。
次回は、あれ?なんだっけ?
「わかってたのに忘れちゃった」のゲームです。
楽しんでいただければと思います。(*^v^)ノシ
次の実験はこちら(都道府県クイズゲーム): 「度忘れ電話注文」
ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
もし応援していただけたら、とても励みになります。






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