実験#37:畑荒らしの天罰回避(嵐のキュウリ畑)

■37個目「嵐のキュウリ畑」

こんにちは、AIと作る100のゲーム実験室の みー です。(^_^)/

今回は、ちょっと個性的で“挑発的”なアクションゲームを作ってみました。
その挑発の主役を務めるのは……がっちり体格の「アカメカッパ」です!

舞台は、激しい嵐が吹き荒れる“キュウリ畑”。 空には怪しげな雷雲が広がり、
大粒の雨が叩きつける中、アカメカッパの過酷な収穫が始まります。

(アカメカッパは、実験#31「カッパの綱引き」で出てきた強敵です。)

(画像:実験#31「カッパの綱引き」)

今回こだわったのは、アカメカッパの「ポーズ」です。(=v=*)

「ただキュウリを拾うだけじゃつまらない。嵐の中でも余裕を見せて、
こちらを挑発してくるようなカッパがいたら面白いかも?」

そんな遊び心から、
移動するたびに「あっかんべー」をしたり「お尻ペンペン」をしたり……。
なんともニヒルで奇抜な動きをするカッパが誕生しました。

キュウリ畑には、おいしそうな“キュウリ”がひょっこり現れますが、油断は禁物です。 上空からは、カッパの大敵・カミナリがドカン!と1段階ずつ迫ってきます。

もし雷に打たれてしまうと……。 アカメカッパが「カミナリ ピリピリ!」状態になって、ビリビリと震えてしまいます。Σ(・□・;)

嵐の音、そして挑発的なBGMに乗りながら、
何本のキュウリを収穫できるでしょうか。

がっちりボディーに鋭い赤目、
そして黄色いくちばしがチャームポイントのカッパと一緒に、
嵐の畑を駆け抜けてみてくださいね!(´v`)

■ AIとのやりとりで面白かった部分

●ゲーム仕様の相談からスタート

みー: (^^*)今日もゲームを作ります。html形式で動かせるゲーム。
落下してくるものを避けるゲームをもとに考えたゲーム。

操作するキャラクターは以前のゲームで出てきた「アカメカッパ」。 がっちりとした体格で目が赤い。

ステージはキュウリ畑。 上から雷が降ってくる。
キュウリ畑にはランダムで“キュウリ”(絵文字があればそれを利用)が現れ、
2秒ぐらいで消える。 キュウリを取るとスコアになる。

アカメカッパはステージ内の 5つのポイント を左右に移動できる。
キュウリもその5ポイントに出現する。

雷は上空から5つのポイントに分かれて落ちてくる。
アカメカッパがいるポイントに雷が来ると感電してゲームオーバー。

一定スコアごとに、キュウリと雷の出現ペースが速くなる。
リトライもできて、スコアランキングもあります。

●AIの反応

AI: お疲れ様です!「アカメカッパ」が主役のキュウリ畑サバイバル、熱いですね。
がっちり体格で赤い目のアカメカッパが、5つのレーンを俊敏に動き回りながら、
雷を避けて大好物のキュウリを収穫する……
シュールで躍動感のあるゲームになりそうです。

●キャラ表現の調整

みー: (^_^)/ ポイントごとのポーズはもっと色々面白いポーズにしよう。 ゲームオーバーの時の“電撃死”は表現がよくない。(死ぬわけではありません)

手足を丸みのある感じにしましょう。 上空にはカミナリグモを表現して、雰囲気作りに雨も降らせられるかな。

AI: 了解いたしました! アカメカッパのキャラクター性をより可愛らしく、そしてステージの雰囲気をカミナリグモと雨でさらに強化します。 これで、嵐のキュウリ畑で奮闘するアカメカッパの様子がより魅力的に表現できたと思います。

●細かい修正とツッコミ

みー: (^.^)蜘蛛ではなく、雷雲です。* 雨が少しわかりにくいかな。 ポーズはもっと大きく変えてもいいと思う。 カミナリピリピリの表現いいと思う。オノマトペいい感じに使えてます。

AI: お疲れ様です!雷雲と雨、そしてアカメカッパのポーズ、承知いたしました! 蜘蛛を黒くてモコモコした雷雲に変更し、雨をより嵐らしく激しく、 カッパのポーズを5カ所それぞれで全く異なる、個性的で大胆なものに強化します。

●挑発ポーズの強化

みー: (*^w^)アカメカッパのポーズを5か所それぞれ、こちらを挑発するような奇抜なのにして。

AI: お疲れ様です! アカメカッパのポーズを、キュウリへの執着と、こちらへの底意地の悪さが入り混じった、 最高に奇抜で挑発的なものに変更しました。

■ 実際の動作

まずは、AIと相談しながら作ってもらったゲームの初期状態です。
(画像:AIが最初に作った基本のベース画面)

スタート画面には、操作説明もわかりやすく表示されています。

(画像:仕上がりのスタート画面)

操作はとてもシンプルで、 5つのポイントを左右に移動するだけ です。

(画像:左右操作での移動)

移動中のアカメカッパは、
なんとも言えない「あっかんべー」ポーズを見せてきます。
この挑発的な動きがクセになります。

(画像:挑発しながら移動するアカメカッパ)

雷を避け損ねると、アカメカッパが「カミナリ ピリピリ!」状態に。
この瞬間がゲームオーバーです。

(画像:カミナリ ピリピリ)


■ コードのポイント

今回の記事では、AI と相談しながら作った 「嵐のキュウリ畑」 の HTML コードを公開します。

ブラウザだけで動く、とてもシンプルなゲームです。
難しい部分はほとんどないので、気軽に試してみてくださいね。

【遊ぶまでの手順】

1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「coffee.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。

🥒 操作方法

操作は キーボードの矢印キーだけ!

  • 「←」左矢印キー:左のポイントへ移動
  • 「→」右矢印キー:右のポイントへ移動

アカメカッパは畑にある 5つのポイント のどこかに立っています。 流れてくるキュウリや雷に合わせて、素早く場所を入れ替えましょう。(=v=*)

⚡ ルールとコツ

ルールはとっても簡単です。(´v`)

● キュウリを収穫しよう!

足元に 🥒(キュウリ)が現れたら、そこに移動してゲット! 収穫するたびにスコアが「+1」されて、カッパくんが喜んで(?)挑発してきます。

● カミナリを避けよう!

上空の雷雲から ⚡(カミナリ)が ガクッ、ガクッ と段階的に落ちてきます。

一番下の地面に届く瞬間にその場所にいると…… アカメカッパが 「カミナリ ピリピリ!」 状態になってゲームオーバー。

落ちてくるリズムをよく見て、間一髪でかわしてくださいね。Σ(・□・;)

● レベルアップに注意!

収穫数が増えるごとに Storm Level が上がっていきます。

レベルが上がると

  • 雨が激しくなる
  • カミナリの落下スピードが速くなる

まさに嵐の本番です!( > < *)

カッパくんの 奇抜な挑発ポーズ を全部見られるまで、ぜひ何度も挑戦してみてくださいね。 最高スコアを目指して、レッツ収穫!(^_^)v

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>アカメカッパ:嵐のキュウリ畑</title>
<style>
body { background: #000000; color: #fff; font-family: 'Helvetica Neue', Arial, sans-serif; text-align: center; margin: 0; overflow: hidden; user-select: none; }
.info { font-size: 1.5rem; margin: 10px; color: #55ff55; text-shadow: 0 0 8px rgba(0,255,0,0.8); font-weight: bold; letter-spacing: 2px; }
canvas {
background: linear-gradient(to bottom, #000000 0%, #050510 60%, #1a1a2e 75%, #1d3d1a 80%, #2d5a27 100%);
border: 4px solid #555;
display: block;
margin: 0 auto 10px;
cursor: pointer;
box-shadow: 0 0 20px rgba(85,255,85,0.15);
}
#ui { position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.95); padding: 40px; border: 3px solid #ff9900; border-radius: 15px; display: none; z-index: 10; min-width: 300px; box-shadow: 0 0 40px rgba(255,153,0,0.6); }
button { padding: 12px 30px; font-size: 1.2rem; cursor: pointer; background: #c0392b; color: white; border: 1px solid #e74c3c; border-radius: 8px; transition: 0.2s; font-weight: bold; margin-top: 20px; box-shadow: 0 4px 0 #8e2a20; }
button:hover { background: #e74c3c; transform: translateY(2px); box-shadow: 0 2px 0 #8e2a20; }
button:active { transform: translateY(4px); box-shadow: 0 0 0 #8e2a20; }
table { margin: 15px auto; width: 90%; border-collapse: collapse; font-size: 1.1rem; }
td { padding: 8px; border-bottom: 1px dashed #555; }
</style>
</head>
<body>
<div class="info">Harvest: <span id="score">0</span> | Storm Level: <span id="level">1</span></div>
<canvas id="gameCanvas" width="700" height="750"></canvas>
<audio id="bgm" src="bgm.mp3" loop preload="auto"></audio>
<div id="ui">
<h1 style="color:#ff9900; margin-top:0; text-shadow: 0 0 10px red;">⚡ カミナリ ピリピリ! ⚡</h1>
<p style="font-size: 1.3rem;">収穫数: <span id="finalScore" style="color:#55ff55; font-size:1.8rem; font-weight:bold;">0</span> 本</p>
<table id="rankingTable"></table>
<button onclick="resetGame()">再挑戦する</button>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const levelEl = document.getElementById('level');
const ui = document.getElementById('ui');
const bgmAudio = document.getElementById('bgm');
const LANES = 5;
const LANE_WIDTH = canvas.width / LANES;
const FIELD_Y = canvas.height * 0.8;
const PLAYER_Y = canvas.height - 80;
const LIGHTNING_STEPS = 8;
const STEP_HEIGHT = canvas.height / LIGHTNING_STEPS;
let score = 0;
let level = 1;
let gameOver = false;
let playerLane = 2;
let items = [];
let lastSpawnTime = 0;
let rainDrops = [];
let floatingTexts = [];
let gameStarted = false;
let flashTimer = 0;
// ====== 音響システム (Web Audio API) ======
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function initAudio() {
if (!audioCtx) {
audioCtx = new AudioContext();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playMoveSound() {
if(!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'square'; // レトロなピコピコ音
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.05, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.start();
osc.stop(audioCtx.currentTime + 0.1);
}
function playGetSound() {
if(!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine'; // 澄んだピコン音
osc.frequency.setValueAtTime(800, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1600, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.start();
osc.stop(audioCtx.currentTime + 0.1);
}
function playZapSound() {
if(!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sawtooth'; // 荒々しいビリビリ音
osc.frequency.setValueAtTime(100, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(10, audioCtx.currentTime + 0.5);
// 音を歪ませる簡単な工夫
osc.detune.setValueAtTime(Math.random() * 1000, audioCtx.currentTime);
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
osc.start();
osc.stop(audioCtx.currentTime + 0.5);
}
// ==========================================
// キー操作
window.addEventListener('keydown', e => {
if (gameOver || !gameStarted) return;
if (e.key === 'ArrowLeft' && playerLane > 0) {
playerLane--;
playMoveSound();
}
if (e.key === 'ArrowRight' && playerLane < LANES - 1) {
playerLane++;
playMoveSound();
}
});
// クリックでスタート(音声再生の許可をとるためクリック必須)
canvas.addEventListener('click', () => {
initAudio(); // 音声エンジン起動
if (!gameStarted && !gameOver) {
gameStarted = true;
lastSpawnTime = Date.now();
bgmAudio.volume = 0.5; // BGMの音量調整
bgmAudio.play().catch(e => console.log("BGMファイルがありません"));
requestAnimationFrame(gameLoop);
}
});
function spawnRain() {
rainDrops.push({
x: Math.random() * canvas.width,
y: -30,
speed: 15 + Math.random() * 15,
length: 30 + Math.random() * 25,
width: 1.5 + Math.random() * 2
});
}
function spawnItem() {
const lane = Math.floor(Math.random() * LANES);
if (Math.random() < 0.45 + (level * 0.06)) {
items.push({
type: 'lightning', lane, step: 0, moveTimer: 0,
speed: Math.max(3, 35 - (level * 2))
});
}
if (Math.random() < 0.35 + (level * 0.03)) {
const cLane = Math.floor(Math.random() * LANES);
items.push({ type: 'cucumber', lane: cLane, timer: 120 });
}
}
function update() {
if (gameOver || !gameStarted) return;
const now = Date.now();
let spawnInterval = Math.max(120, 800 - (level * 50));
if (now - lastSpawnTime > spawnInterval) {
spawnItem();
lastSpawnTime = now;
}
level = Math.floor(score / 5) + 1;
levelEl.innerText = level;
if (Math.random() < 0.6 + (level * 0.15)) spawnRain();
for (let i = rainDrops.length - 1; i >= 0; i--) {
rainDrops[i].y += rainDrops[i].speed;
if (rainDrops[i].y > canvas.height) rainDrops.splice(i, 1);
}
for (let i = floatingTexts.length - 1; i >= 0; i--) {
floatingTexts[i].y -= 2;
floatingTexts[i].timer--;
if (floatingTexts[i].timer <= 0) floatingTexts.splice(i, 1);
}
for (let i = items.length - 1; i >= 0; i--) {
let it = items[i];
if (it.type === 'lightning') {
it.moveTimer++;
if (it.moveTimer >= it.speed) { it.step++; it.moveTimer = 0; }
let currentY = it.step * STEP_HEIGHT;
if (it.lane === playerLane && currentY > PLAYER_Y - 80 && currentY < PLAYER_Y + 20) {
gameOver = true;
flashTimer = 10;
playZapSound(); // 雷のビリビリ音!
bgmAudio.pause(); // やられたらBGMを止める
showEnd();
}
if (currentY > canvas.height) items.splice(i, 1);
} else {
it.timer--;
if (it.lane === playerLane && it.timer > 0) {
score++;
scoreEl.innerText = score;
playGetSound(); // 収穫のピコン音!
floatingTexts.push({
x: it.lane * LANE_WIDTH + LANE_WIDTH / 2,
y: PLAYER_Y - 80,
timer: 30
});
items.splice(i, 1);
} else if (it.timer <= 0) items.splice(i, 1);
}
}
}
function drawThunderCloudRows() {
ctx.save();
ctx.translate(canvas.width / 2, 80);
ctx.fillStyle = '#111';
const numClouds = 10;
const cloudWidth = (canvas.width / numClouds) * 1.5;
for (let i = 0; i < numClouds; i++) {
const x = -canvas.width / 2 + i * (canvas.width / numClouds) + cloudWidth / 2;
ctx.beginPath();
ctx.ellipse(x - 50, 0, 40, 40, 0, 0, Math.PI * 2); ctx.fill();
ctx.ellipse(x, -20, 50, 50, 0, 0, Math.PI * 2); ctx.fill();
ctx.ellipse(x + 50, 0, 40, 40, 0, 0, Math.PI * 2); ctx.fill();
ctx.ellipse(x, 20, 45, 45, 0, 0, Math.PI * 2); ctx.fill();
}
ctx.restore();
}
function drawKappa(x, y, lane) {
const kColor = '#3a6332';
const beakColor = '#ffcc00';
const shellColor = '#1d3d1a';
ctx.save();
ctx.translate(x, y);
let armLeft = { angle: 0, x: -30, y: -50, width: 15 };
let armRight = { angle: 0, x: 30, y: -50, width: 15 };
let legLeft = { x: -20, y: -10, width: 10, height: 15 };
let legRight = { x: 10, y: -10, width: 10, height: 15 };
let headTilt = 0;
let headShiftY = 0;
let bodyScale = { w: 1, h: 1 };
let dishTilt = 0;
let beakTilt = 0;
let bodyAngle = 0;
let showTongue = false;
if (gameOver) {
const flicker = Date.now() % 300 < 100 ? beakColor : (Date.now() % 300 < 200 ? 'white' : 'black');
bodyScale.w *= (0.9 + Math.random() * 0.2); bodyScale.h *= (0.9 + Math.random() * 0.2);
ctx.fillStyle = flicker;
} else {
ctx.fillStyle = kColor;
}
switch(lane) {
case 0: bodyAngle = -0.1; armLeft.y = -60; armLeft.x = -15; armLeft.angle = -0.8; armRight.y = -50; armRight.x = 35; armRight.angle = 1.0; headTilt = -0.3; dishTilt = -0.3; beakTilt = -0.2; showTongue = true; break;
case 1: bodyAngle = 0.3; bodyScale.h = 0.8; armLeft.y = -40; armLeft.x = -10; armLeft.angle = 1.2; armRight.y = -40; armRight.x = 10; armRight.angle = 1.2; headShiftY = 10; headTilt = 0.2; dishTilt = 0.1; break;
case 2: bodyScale.w = 1.1; armLeft.y = -55; armLeft.x = -35; armLeft.angle = -0.3; armRight.y = -65; armRight.x = 40; armRight.angle = -1.2; break;
case 3: bodyScale.w = 0.9; bodyScale.h = 0.9; armLeft.y = -60; armLeft.x = -10; armLeft.angle = 1.2; armRight.y = -60; armRight.x = 10; armRight.angle = 1.2; headTilt = -0.1; break;
case 4: bodyScale.w = 0.8; armLeft.y = -40; armLeft.x = -25; armLeft.angle = 0.5; armRight.y = -40; armRight.x = 25; armRight.angle = 0.5; headTilt = 0.2; dishTilt = 0.2; break;
}
if (!gameOver) ctx.fillStyle = kColor;
ctx.beginPath(); ctx.ellipse(legLeft.x, legLeft.y, legLeft.width, legLeft.height, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(legRight.x, legRight.y, legRight.width, legRight.height, 0, 0, Math.PI*2); ctx.fill();
const bodyW = 60 * bodyScale.w;
const bodyH = 60 * bodyScale.h;
ctx.save();
ctx.rotate(bodyAngle);
ctx.fillRect(-bodyW / 2, -65 + headShiftY, bodyW, bodyH);
if (!gameOver) ctx.fillStyle = shellColor;
if (gameOver) ctx.globalAlpha = 0.5;
const shellW = 44 * bodyScale.w;
const shellH = 50 * bodyScale.h;
ctx.fillRect(-shellW / 2, -60 + headShiftY, shellW, shellH);
ctx.restore();
if (gameOver) ctx.globalAlpha = 1;
if (!gameOver) ctx.strokeStyle = kColor;
if (gameOver) ctx.strokeStyle = ctx.fillStyle;
ctx.lineWidth = armLeft.width; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(armLeft.x, armLeft.y + headShiftY);
ctx.lineTo(armLeft.x - 15, armLeft.y + 10 + headShiftY);
ctx.lineTo(armLeft.x - 5 + Math.sin(armLeft.angle)*15, armLeft.y + 25 + Math.cos(armLeft.angle)*15 + headShiftY); ctx.stroke();
ctx.lineWidth = armRight.width;
ctx.beginPath(); ctx.moveTo(armRight.x, armRight.y + headShiftY);
ctx.lineTo(armRight.x + 15, armRight.y + 10 + headShiftY);
ctx.lineTo(armRight.x + 5 + Math.sin(armRight.angle)*15, armRight.y + 25 + Math.cos(armRight.angle)*15 + headShiftY); ctx.stroke();
ctx.translate(0, -75 + headShiftY);
ctx.rotate(headTilt);
if (!gameOver) ctx.fillStyle = kColor;
if (gameOver) ctx.fillStyle = ctx.fillStyle;
ctx.beginPath(); ctx.arc(0, 0, 25, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = 'red';
ctx.beginPath(); ctx.arc(-10, -5, 7, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(10, -5, 7, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = beakColor;
ctx.save();
ctx.rotate(beakTilt);
ctx.beginPath();
ctx.moveTo(0, 5);
ctx.lineTo(-10, 15);
ctx.lineTo(10, 15);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#bfa100';
ctx.beginPath();
ctx.ellipse(0, 15, 10, 3, 0, 0, Math.PI*2);
ctx.fill();
if (showTongue && !gameOver) {
ctx.fillStyle = '#ff6688';
ctx.beginPath();
ctx.ellipse(5, 18, 4, 8, -0.2, 0, Math.PI*2);
ctx.fill();
}
ctx.restore();
ctx.fillStyle = '#b3ffb3';
ctx.beginPath(); ctx.ellipse(0, -22, 18, 5, dishTilt, 0, Math.PI*2); ctx.fill();
if (gameOver) {
ctx.strokeStyle = beakColor; ctx.lineWidth = 3; ctx.lineCap = 'round';
for (let i = 0; i < 12; i++) {
const angle = (Math.PI * 2 / 12) * i + Math.random() * 0.3;
const startX = Math.sin(angle) * 35;
const startY = Math.cos(angle) * 35;
const sparkLen = 15 + Math.random() * 25;
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startX + Math.sin(angle) * sparkLen * 0.5, startY + Math.cos(angle) * sparkLen * 0.5 + Math.random() * 5);
ctx.lineTo(startX + Math.sin(angle + 0.2) * sparkLen * 0.8, startY + Math.cos(angle + 0.2) * sparkLen * 0.8 + Math.random() * 8);
ctx.lineTo(startX + Math.sin(angle - 0.1) * sparkLen, startY + Math.cos(angle - 0.1) * sparkLen + Math.random() * 10);
ctx.stroke();
}
}
ctx.restore();
}
function drawRain() {
ctx.strokeStyle = "rgba(150, 180, 255, 0.6)";
ctx.lineCap = 'round';
for (const rd of rainDrops) {
ctx.lineWidth = rd.width;
ctx.beginPath(); ctx.moveTo(rd.x, rd.y); ctx.lineTo(rd.x, rd.y + rd.length); ctx.stroke();
}
}
function drawTitle() {
ctx.fillStyle = "rgba(0,0,0,0.85)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold 45px sans-serif'; ctx.fillStyle = '#fff';
ctx.fillText('アカメカッパ:嵐のキュウリ畑', canvas.width / 2 - 310, canvas.height / 2 - 120);
ctx.font = 'bold 30px sans-serif'; ctx.fillStyle = '#ffcc00';
ctx.shadowBlur = 10; ctx.shadowColor = '#ff9900';
if (Date.now() % 1000 < 500) {
ctx.fillText('画面をクリックして開始', canvas.width / 2 - 170, canvas.height / 2 + 20);
}
ctx.shadowBlur = 0;
ctx.font = '20px sans-serif'; ctx.fillStyle = '#ddd';
ctx.fillText('← → : 移動 🥒 : 収穫 ⚡ : 避ける', canvas.width / 2 - 170, canvas.height / 2 + 90);
drawKappa(canvas.width / 2, PLAYER_Y, 2);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!gameStarted) {
drawTitle();
return;
}
if (flashTimer > 0) {
ctx.fillStyle = 'rgba(255, 255, 255, ' + (flashTimer / 10) + ')';
ctx.fillRect(0, 0, canvas.width, canvas.height);
flashTimer--;
}
drawRain();
drawThunderCloudRows();
ctx.fillStyle = '#1a3317';
for(let i=0; i<LANES; i++) {
ctx.beginPath();
ctx.ellipse(i*LANE_WIDTH + LANE_WIDTH/2, FIELD_Y + 60, LANE_WIDTH/2, 20, 0, 0, Math.PI*2);
ctx.fill();
}
ctx.strokeStyle = '#0a1c07'; ctx.lineWidth = 4;
for(let i=0; i<15; i++) {
ctx.beginPath();
ctx.moveTo(i*50, FIELD_Y);
ctx.bezierCurveTo(i*50+20, FIELD_Y-20, i*50-20, FIELD_Y+40, i*50+40, FIELD_Y+20);
ctx.stroke();
}
items.forEach(it => {
const x = it.lane * LANE_WIDTH + LANE_WIDTH / 2;
if (it.type === 'lightning') {
const y = it.step * STEP_HEIGHT;
ctx.shadowBlur = 15; ctx.shadowColor = "yellow";
ctx.font = '60px serif'; ctx.fillText('⚡', x - 30, y);
ctx.shadowBlur = 0;
} else {
ctx.font = '45px serif';
ctx.globalAlpha = (it.timer < 30) ? (it.timer%10<5?0.3:1) : 1;
ctx.fillText('🥒', x - 22, PLAYER_Y + 10);
ctx.globalAlpha = 1;
}
});
drawKappa(playerLane * LANE_WIDTH + LANE_WIDTH / 2, PLAYER_Y, playerLane);
floatingTexts.forEach(ft => {
ctx.font = 'bold 24px sans-serif';
ctx.fillStyle = `rgba(85, 255, 85, ${ft.timer / 30})`;
ctx.shadowBlur = 5; ctx.shadowColor = "black";
ctx.fillText('+1', ft.x - 15, ft.y);
ctx.shadowBlur = 0;
});
}
function showEnd() {
ui.style.display = 'block';
document.getElementById('finalScore').innerText = score;
let ranking = JSON.parse(localStorage.getItem('kappa_rank') || '[]');
ranking.push(score);
ranking.sort((a,b)=>b-a);
ranking = ranking.slice(0,5);
localStorage.setItem('kappa_rank', JSON.stringify(ranking));
document.getElementById('rankingTable').innerHTML = ranking.map((s,i)=>`<tr><td>${i+1}位</td><td style="text-align:right;">${s} 本</td></tr>`).join('');
}
function resetGame() {
score = 0; level = 1; items = []; playerLane = 2; gameOver = false;
scoreEl.innerText = '0';
ui.style.display = 'none';
rainDrops = [];
floatingTexts = [];
flashTimer = 0;
lastSpawnTime = Date.now();
bgmAudio.currentTime = 0;
bgmAudio.play().catch(e => {}); // BGMも最初から
requestAnimationFrame(gameLoop);
}
function gameLoop() {
update(); draw();
if (!gameOver && gameStarted) {
requestAnimationFrame(gameLoop);
} else if (gameOver && flashTimer > 0) {
requestAnimationFrame(gameLoop);
}
}
draw();
</script>
</body>
</html>


■ 今日の学び

今回のゲームは、実験 #2「丸太郎と突撃猪」を作ったときに AIと相談した 電子ゲーム風のイメージ を、もう一度再現してみた作品です。

操作ボタンを押すとピコピコ動くキャラクター、 そしてシンプルなルール。
あの“電子ゲームらしさ”がしっかり再現できたので、
とても満足しています。( * ⌒ _ ⌒ * )

ゲームの仕上げとして、効果音とBGMをAIに相談してみたところ……
なんと今回は AIが気まぐれに音楽を生成してくれました。


● AIとのやりとり(音楽編)

(^^)効果音とBGM入れたいね。テーマは「雷カッパの挑発」で。

(AI)
ゲーム作りもいよいよ大詰め。音を入れると一気に化けるからね。
今回は「雷カッパの挑発」がテーマのBGMと効果音(SE)だね。任せて!


AIが作ってくれたBGMがこちら。 まさかの “生意気で挑発的なメロディ” が完成していて、思わず笑ってしまいました。

(画像:AIが生成したBGMの画面)

🎧 BGM:Lightning Symphony (AI生成)

なんだかかっこいいの出来上がってびっくり Σ(@o@;)!
AIに音楽まで作らせる時代が来たんだなぁ……と、しみじみ感じた瞬間でした。



■ 次回は、しょうが君の新たな冒険が始まります

AIと相談しながら、奥行きのある世界観のアクションゲーム を作ってみました。
まるで絵本の中に入り込んだような、
不思議でやさしい雰囲気の世界観が気に入っています。

次回もお楽しみに。(^v^)ノシ

次の実験はこちら(時を駆けるしょうが君と巨大タンタン):
「Time & Sky Adventure 」



 今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)