実験 #21:自然を育てる小さな力(ミミズの耕し)

■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」など好きな名前で保存してください。

 保存したファイルをブラウザで開くだけで遊べます。

【操作方法】
・難易度を選びます。
・矢印キーでミミズの向きを操作します。

▼ここからコード▼(クリックで開く)
<!DOCTYPE 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^*)
 もし応援していただけたら、とても励みになります。

~このブログについて~

AIと一緒に遊び、
100の実験でゲームを育てるブログ

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


AIと作る100のゲーム実験室(*^v^*)をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む