実験#100:わんぱく丸太郎の自由すぎる冒険(保護サークル・サバイバル:丸太郎と突撃猪)

■100個目 「保護サークル・サバイバル:丸太郎と突撃猪」

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

今回は、ちょこまかと動き回るキャラクターを
“守り抜く”サバイバルゲームを作ってみました。

主人公は、白くて丸い体につぶらな瞳をした「丸太郎」。

見た目は可愛いのに、
V字の眉毛とニヤッと笑う口元がチャームポイントの、
ちょっと腕白で生意気な子です。(=v=*)

舞台は、木漏れ日が差し込む明るい広葉樹の森。

丸太郎がのんびり散歩していると、
森の奥から突然「ドッドッドッ」と地鳴りが響き、
イノシシの大群が猛スピードで突進してきます!(><;)

しかも丸太郎は、危険なんて気にしない性格。

あっちへふらふら、こっちへちょろちょろ、
気まぐれに走り回るので目が離せません。

プレイヤーは、そんな手のかかる丸太郎がイノシシに踏みつぶされないよう、
過保護なくらいに守ってあげる必要があります。

森の中には、取ると良いことが起きる
キラキラした不思議なアイテムも飛んできます。

「取りに行きたいけど、丸太郎から離れると危ない……!」という、
ハラハラするジレンマもこのゲームの魅力です。(´艸`)

手のかかる子ほど可愛いと言いますが、
無事に守り切れたときの愛着はひとしおですよ。

気になる操作方法や遊び方については、
後ほど詳しくご紹介していきますね!(=v=*)


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

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

みー:(^v^) 今日もゲームを作ります。HTMLで動かせるゲーム。

勝手に動き回るキャラクターに対して、
マウスカーソルで保護サークルをかけるゲームです。

サークルから外れるとバチバチとダメージを受けてしまう……
中毒性のある感じにできないかな。

AI: いらっしゃい!今日も元気だねえ。

マウスカーソルで「保護サークル」を作って、動き回る対象を守るゲーム。
このアイデア、すごくいいよ。

マウスでターゲットを追い続ける
“トラッキング系”のアクションは、
もともと中毒性がめちゃくちゃ高いジャンルなんだ。

そこに「サークルから外れた時の強烈なペナルティ(バチバチ感)」と
「徐々に上がる難易度」を加えれば、
やめどきを見失うゲームになるね。

みー:(*^_^)ダメージ波攻撃も入れたいです。

波に安全ゾーン外で当たると追加ダメージ。
逆に、一定時間安全ゾーンを維持すると回復ボーナス。

主人公は丸太郎にしましょう。
白い体に円らな瞳、腕白な感じのキャラクター。

AI: 「丸太郎」!いい名前じゃないか。

白くてつぶらな瞳なのに、ちょっと腕白で生意気な顔つき……
こういう手のかかるヤツほど、守り切った時の愛着が湧くんだよね。

「ダメージ波」と「保護ボーナス」の追加も最高。
波が来るたびに「絶対にサークルから出すな!」という緊張感が生まれるし、
ボーナスが入った時の「よっしゃ!」が気持ちいい。

みー:(=v=) アイテム要素も入れましょう。

  • 白:点(小)
  • 青:点(小)+スロー
  • 金:点(大)
  • 虹:点(大)+無敵

丸太郎からサークルを外さないといけないけど、
画面端を跳ね返りながら飛ぶ丸いアイテムを
マウスカーソルで触れて取る感じにできますか?

AI: いいねえ、乗ってきたね!

その「あっちもこっちも気にしなきゃいけないジレンマ」、
ゲームを面白くする最高のスパイスだよ。

丸太郎を守り続けなきゃいけないのに、
画面の端に美味しそうなアイテムがフワフワ飛んでくる……。

「今なら取れるか? いや、丸太郎が急カーブしたら危ないぞ……!」
という一瞬の葛藤が、プレイヤーの脳汁をドバドバ出すんだ。

ご所望の4種類のアイテムと特殊効果も実装しておいたよ。
アイテムを取るためにサークルを動かすから、
ゲームの動線が一気に熱くなるね。

みー:(^_^) 波をイノシシの大群にできますか?

AI: いらっしゃい!そのアイデアは最高に面白い!

ただの赤い波が、画面を埋め尽くす「イノシシの大群」になるわけだね。
腕白な丸太郎が、
突進してくるイノシシたちに踏みつぶされないように守る……。

一気に絵面が賑やかで、コミカルかつスリリングになるよ。

よし、「赤い波ギミック」を丸ごと「イノシシ突進」に書き換えたよ。

みー:(^_^) 背景が黒いと暗いので、
広葉樹の木漏れ日の明るい山の中のイメージにできますか?

保護サークルをほんの少しサイズアップできるかな。

AI: 雰囲気を変えたいんだな。
「木漏れ日が差す広葉樹の森」……最高じゃないか!

丸太郎も猪たちも山の生き物だから、舞台としてぴったりだよ。

背景を明るい緑と木漏れ日のイメージに変更して、
保護サークルの半径も少し(60 → 70)広げておいたよ。


■ 実際の動作

まずは、AIが最初に作ってくれた初期バージョンのゲーム画面です。

保護サークルから丸太郎がはみ出すと、
痛そうなパーティクルがバチバチと飛び散ります(><)

(画像:AIが最初に出してくれた初期画面)



こちらが、最終的に仕上がったゲームのスタート画面です。
マウス操作だけで遊べる、とてもシンプルなゲームになりました。

(画像:完成版のスタート画面)


前作のゲームの要素も引き継いでいます。

突撃猪:イノシシの集団

主人公:丸太郎

丸いアイテム

白:得点(小)

青:得点(小)+スロー

金:得点(大)

虹:得点(大)+無敵

(画像:ゲーム画面)


青い丸を取ると、丸太郎の動きがゆっくりになります。

一定時間、保護サークル内から出さずに守り続けると回復するので、
ダメージを取り返すチャンスになります。

(画像:スロー状態の丸太郎)


虹色の丸を取ると、丸太郎は無敵状態になります。
この間は丸太郎を気にせず、アイテムを取り放題です。

(画像:無敵状態の丸太郎)


■ コードのポイント

今回の記事では、
AI と相談しながら作った「保護サークル・サバイバル:丸太郎と突撃猪」 の HTML コードを公開します。

このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「marutarou2.html」など、好きな名前で保存します。

(※拡張子は .html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。

■ 操作方法

操作はとっても簡単です!(^_^)
キーボードは使いません。マウスを動かすだけで遊べます。

画面の中のマウスカーソルが、
丸太郎を守る「保護サークル」になっています。

気まぐれに走り回る丸太郎がサークルの外に出ないように、
マウスで優しく追いかけてあげてくださいね。(^^*)

⚠️ サークルから外れると…

丸太郎がサークルの外に出てしまうと、
「ビババッ!」とダメージを受けて体力が減ってしまいます。(><;)

油断すると一瞬でピンチになるので、しっかり見守ってあげてください。

✨ ずっと守ってあげると…

逆に、サークルの中にずっと入れてあげると、
サークルの周りの緑色のゲージが溜まっていきます。

ゲージが一周すると体力が回復して、ボーナスポイントもゲット!

「よしよし、よくできたね」って撫でてあげるような感覚です。(=v=*)

🐗 突撃猪(イノシシ)に注意!

時々、画面に 「⚠️ BOAR STAMPEDE!」 という警告が出たあと、
イノシシの大群がドコドコと突進してきます!

この時にサークルの外にいると、
丸太郎は大ダメージを受けてしまいます。

でも、サークルの中にいれば「カキィィン!」と
安全にガードできます。(`・ω・´)

🎁 不思議なアイテムたち

画面をフワフワと飛んでいる色付きの玉は、
サークルの「フチ」を触れさせるだけでゲットできます。

  • ⚪️ 白アイテム:100点ゲット!
  • 🔵 青アイテム:一定時間、丸太郎の動きがゆっくりに。
    落ち着いてゲージを溜める大チャンス✨
  • 🟡 金アイテム:なんと500点ゲット!
  • 🌈 虹アイテム:丸太郎がオーラを纏って無敵モードに。
    サークルから外れても、イノシシにぶつかってもへっちゃらです。(´艸`)

アイテムを取りに行くためにサークルを動かすか、
それとも丸太郎のそばにぴったりくっついて守るか……。

簡単操作の中に隠れたハラハラドキドキのジレンマを、
ぜひ体験してみてくださいね!(^^*)


“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Protection Circle Survival - Sunlight Forest</title>
<style>
body {
margin: 0;
/* ページ全体も森っぽい深い緑に */
background: linear-gradient(135deg, #2e7d32, #1b5e20);
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: white;
user-select: none;
}
#gameContainer {
position: relative;
}
canvas {
/* 木漏れ日の明るい広葉樹林をCSSグラデーションで表現 */
background-color: #aed581;
background-image:
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.6) 0%, transparent 20%),
radial-gradient(circle at 75% 15%, rgba(255, 255, 255, 0.5) 0%, transparent 25%),
radial-gradient(circle at 40% 70%, rgba(255, 255, 255, 0.4) 0%, transparent 30%),
radial-gradient(circle at 85% 80%, rgba(255, 255, 255, 0.5) 0%, transparent 20%);
border: 4px solid #336600;
border-radius: 8px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.6);
cursor: none;
}
#ui {
position: absolute;
top: 15px;
left: 20px;
pointer-events: none;
width: 560px;
display: flex;
justify-content: space-between;
}
#leftUi {
display: flex;
flex-direction: column;
}
#healthBarBg {
width: 200px;
height: 15px;
background: #222;
border: 2px solid #fff;
border-radius: 8px;
overflow: hidden;
margin-top: 5px;
box-shadow: 0 0 5px rgba(0,0,0,0.8);
}
#healthBar {
width: 100%;
height: 100%;
background: #00ff88;
transition: background 0.2s;
}
#score {
font-size: 24px;
font-weight: bold;
color: #fff;
/* 背景が明るいので黒い縁取りを強めに */
text-shadow: 2px 2px 4px rgba(0,0,0,0.9), -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
}
#statusEffects {
position: absolute;
top: 70px;
left: 20px;
font-size: 16px;
font-weight: bold;
pointer-events: none;
}
.effect-badge {
padding: 4px 8px;
border-radius: 4px;
margin-bottom: 5px;
display: none;
box-shadow: 0 0 5px rgba(0,0,0,0.8);
border: 1px solid #fff;
}
#slowBadge { background: #3399ff; color: white; }
#invincibleBadge { background: linear-gradient(45deg, #ff3366, #ff9933, #33ccff, #9933ff); color: white; }
#warningText {
font-size: 22px;
font-weight: bold;
color: #ff3300;
text-shadow: 2px 2px 0px #fff, -1px -1px 0px #fff, 1px -1px 0px #fff, -1px 1px 0px #fff;
opacity: 0;
transition: opacity 0.1s;
}
.overlay {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 50, 20, 0.85); /* 森っぽい暗幕 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
border-radius: 4px;
z-index: 10;
}
#startScreen h1 { font-size: 36px; color: #aed581; margin-bottom: 10px; text-shadow: 2px 2px 4px #000;}
#messageBox h1 { margin: 0 0 10px 0; color: #ff3366; text-shadow: 2px 2px 4px #000;}
p { font-size: 18px; margin-bottom: 20px; text-shadow: 1px 1px 2px #000; }
.blink { animation: blinker 1.5s linear infinite; font-size: 16px; color: #ddd; cursor: pointer; font-weight:bold; }
@keyframes blinker { 50% { opacity: 0; } }
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" width="600" height="600"></canvas>
<div id="ui">
<div id="leftUi">
<div id="score">Score: 0</div>
<div id="healthBarBg"><div id="healthBar"></div></div>
</div>
<div id="warningText">⚠️ BOAR STAMPEDE!</div>
</div>
<div id="statusEffects">
<div id="slowBadge" class="effect-badge">丸太郎スロー中</div>
<div id="invincibleBadge" class="effect-badge">無敵モード!</div>
</div>
<div id="startScreen" class="overlay">
<h1>丸太郎と突撃猪</h1>
<p>マウスで保護サークルを操り、丸太郎を守り抜け!</p>
<p class="blink">画面をクリックしてスタート</p>
</div>
<div id="messageBox" class="overlay" style="display: none;">
<h1>GAME OVER</h1>
<p>丸太郎は力尽きた...</p>
<div id="finalScore" style="font-size: 32px; font-weight: bold; margin-bottom: 20px; color: #00ff88; text-shadow: 2px 2px 4px #000;"></div>
<p class="blink">画面をクリックしてリトライ</p>
</div>
</div>
<script>
let audioCtx;
const Sound = {
init: function() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
},
playOsc: function(freq, type, dur, vol = 0.05, slideFreq = null) {
if (!audioCtx || audioCtx.state !== 'running') return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type; osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
if (slideFreq) osc.frequency.exponentialRampToValueAtTime(slideFreq, audioCtx.currentTime + dur);
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur);
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(); osc.stop(audioCtx.currentTime + dur);
},
noise: function(dur, vol = 0.1, isLow = false) {
if (!audioCtx || audioCtx.state !== 'running') return;
const bufferSize = audioCtx.sampleRate * dur;
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 noise = audioCtx.createBufferSource(); noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter(); filter.type = isLow ? 'lowpass' : 'bandpass'; filter.frequency.value = isLow ? 400 : 1000;
const gain = audioCtx.createGain(); gain.gain.setValueAtTime(vol, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur);
noise.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination); noise.start();
},
bgmStep: 0,
bgmNotes: [196.00, 196.00, 220.00, 196.00, 261.63, 196.00, 293.66, 261.63],
playBGM: function(frames, isSlow, isInvincible) {
if (!audioCtx || audioCtx.state !== 'running' || gameState !== 'playing') return;
let interval = 12;
if (isInvincible) interval = 6; else if (isSlow) interval = 24;
if (frames % interval === 0) {
let note = this.bgmNotes[this.bgmStep % this.bgmNotes.length];
if (isInvincible) note *= 1.5; else if (isSlow) note *= 0.5;
this.playOsc(note, 'triangle', 0.1, 0.03);
if (this.bgmStep % 2 === 0) this.noise(0.05, 0.02, true);
this.bgmStep++;
}
},
coin: () => Sound.playOsc(880, 'sine', 0.1, 0.05, 1200),
heal: () => { Sound.playOsc(523.25, 'sine', 0.1, 0.05); setTimeout(()=>Sound.playOsc(659.25, 'sine', 0.1, 0.05), 100); setTimeout(()=>Sound.playOsc(783.99, 'sine', 0.2, 0.05), 200); },
slow: () => Sound.playOsc(440, 'triangle', 0.5, 0.05, 110),
invincible: () => { Sound.playOsc(440, 'square', 0.1, 0.05, 880); setTimeout(()=>Sound.playOsc(554, 'square', 0.1, 0.05, 1108), 100); setTimeout(()=>Sound.playOsc(659, 'square', 0.2, 0.05, 1318), 200); },
warning: () => Sound.playOsc(150, 'sawtooth', 0.2, 0.08, 200),
damage: () => Sound.noise(0.1, 0.05, false),
stampedeRumble: () => Sound.noise(0.1, 0.08, true),
crash: () => Sound.noise(0.4, 0.2, true),
guard: () => Sound.playOsc(1200, 'square', 0.1, 0.05, 2000)
};
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const healthBar = document.getElementById('healthBar');
const startScreen = document.getElementById('startScreen');
const messageBox = document.getElementById('messageBox');
const finalScoreEl = document.getElementById('finalScore');
const warningText = document.getElementById('warningText');
const slowBadge = document.getElementById('slowBadge');
const invincibleBadge = document.getElementById('invincibleBadge');
let gameState = 'start';
let score = 0, health = 100, frames = 0, shakeTime = 0;
let slowTimer = 0, invincibleTimer = 0, safeFrames = 0;
const MAX_SAFE_FRAMES = 180;
// サークルサイズを60から70に僅かにサイズアップ!
const mouse = { x: 300, y: 300, radius: 70 };
let target = { x: 300, y: 300, radius: 18, vx: 0, vy: 0, speed: 2.5, angle: Math.random() * Math.PI * 2 };
let stampede = { active: false, warning: 0, x: -150, speed: 10, hasHit: false, boars: [] };
const BOAR_COUNT = 12;
let items = [], particles = [], texts = [];
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left; mouse.y = e.clientY - rect.top;
});
document.addEventListener('click', () => {
Sound.init();
if (gameState === 'start' || gameState === 'gameover') {
startScreen.style.display = 'none';
resetGame();
}
});
function initStampedeBoars() {
stampede.boars = [];
for (let i = 0; i < BOAR_COUNT; i++) {
stampede.boars.push({ offsetX: Math.random() * 80 - 40, y: (canvas.height / BOAR_COUNT) * i + (Math.random() * 20 - 10), size: Math.random() * 5 + 20 });
}
}
function resetGame() {
score = 0; health = 100; frames = 0; slowTimer = 0; invincibleTimer = 0; safeFrames = 0;
target.x = 300; target.y = 300; target.speed = 2.5;
stampede.active = false; stampede.warning = 0;
items = []; particles = []; texts = [];
gameState = 'playing';
messageBox.style.display = 'none';
canvas.style.cursor = 'none';
warningText.style.opacity = 0;
slowBadge.style.display = 'none'; invincibleBadge.style.display = 'none';
updateHealthBar();
loop();
}
function updateHealthBar() {
healthBar.style.width = Math.max(0, health) + '%';
if (health > 60) healthBar.style.background = '#00ff88';
else if (health > 30) healthBar.style.background = '#ffcc00';
else healthBar.style.background = '#ff3366';
}
function createSparks(x, y, color = null, count = 5) {
for(let i = 0; i < count; i++) {
particles.push({
x: x, y: y, vx: (Math.random() - 0.5) * 10, vy: (Math.random() - 0.5) * 10,
life: 1, decay: Math.random() * 0.05 + 0.02, color: color || `rgba(255, ${Math.random() * 150 + 50}, 0, `
});
}
}
function addFloatingText(text, x, y, color) { texts.push({ text: text, x: x, y: y, life: 1, color: color }); }
function spawnItem() {
if (items.length >= 4) return;
const types = ['white', 'blue', 'gold', 'rainbow'];
const weights = [0.45, 0.25, 0.20, 0.10];
let r = Math.random(), type = 'white';
for (let i = 0; i < types.length; i++) { r -= weights[i]; if (r <= 0) { type = types[i]; break; } }
const angle = Math.random() * Math.PI * 2, itemSpeed = Math.random() * 1.5 + 1.5;
items.push({ x: Math.random() * (canvas.width - 40) + 20, y: Math.random() * (canvas.height - 40) + 20, radius: 10, type: type, vx: Math.cos(angle) * itemSpeed, vy: Math.sin(angle) * itemSpeed });
}
function drawBoar(x, y, size, frameOffset) {
ctx.save(); ctx.translate(x, y);
ctx.translate(0, Math.sin(frames * 0.5 + frameOffset) * 3);
ctx.fillStyle = '#8b4513'; ctx.beginPath(); ctx.ellipse(0, 0, size, size * 0.7, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#a0522d'; ctx.beginPath(); ctx.ellipse(size * 0.7, size * 0.2, size * 0.4, size * 0.3, Math.PI * 0.1, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(size * 0.6, -size * 0.1, size * 0.15, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(size * 0.65, -size * 0.1, size * 0.08, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#8b4513'; ctx.beginPath(); ctx.moveTo(0, -size * 0.5); ctx.lineTo(size * 0.3, -size * 0.9); ctx.lineTo(size * 0.5, -size * 0.5); ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(size * 0.8, size * 0.3); ctx.quadraticCurveTo(size * 1.1, size * 0.3, size * 1.0, 0); ctx.stroke();
ctx.strokeStyle = '#8b4513'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(-size, 0); ctx.lineTo(-size * 1.3, Math.sin(frames * 0.2 + frameOffset) * 5); ctx.stroke();
ctx.restore();
}
function update() {
if (gameState !== 'playing') return;
frames++;
Sound.playBGM(frames, slowTimer > 0, invincibleTimer > 0);
target.speed += 0.0002;
if (slowTimer > 0) slowTimer--;
if (invincibleTimer > 0) invincibleTimer--;
slowBadge.style.display = slowTimer > 0 ? 'block' : 'none';
invincibleBadge.style.display = invincibleTimer > 0 ? 'block' : 'none';
let currentSpeed = target.speed * (slowTimer > 0 ? 0.4 : 1);
if (Math.random() < 0.03 + (frames * 0.00001)) target.angle += (Math.random() - 0.5) * Math.PI * 1.2;
target.vx = Math.cos(target.angle) * currentSpeed; target.vy = Math.sin(target.angle) * currentSpeed;
target.x += target.vx; target.y += target.vy;
if (target.x - target.radius < 0 || target.x + target.radius > canvas.width) { target.vx *= -1; target.x = Math.max(target.radius, Math.min(canvas.width - target.radius, target.x)); target.angle = Math.atan2(target.vy, target.vx); }
if (target.y - target.radius < 0 || target.y + target.radius > canvas.height) { target.vy *= -1; target.y = Math.max(target.radius, Math.min(canvas.height - target.radius, target.y)); target.angle = Math.atan2(target.vy, target.vx); }
const isSafe = Math.hypot(target.x - mouse.x, target.y - mouse.y) + target.radius <= mouse.radius;
if (isSafe || invincibleTimer > 0) {
safeFrames++; score += currentSpeed * 0.5;
if (safeFrames >= MAX_SAFE_FRAMES) {
health = Math.min(100, health + 24); score += 500; safeFrames = 0; Sound.heal();
createSparks(target.x, target.y, `rgba(0, 255, 136, `); addFloatingText("HEAL & BONUS!", target.x, target.y - 30, "#00ff88"); updateHealthBar();
}
} else {
safeFrames = 0; health -= 0.6;
if (frames % 6 === 0) { createSparks(target.x, target.y); Sound.damage(); }
updateHealthBar();
}
if (frames % 150 === 0) spawnItem();
for (let i = items.length - 1; i >= 0; i--) {
let item = items[i]; item.x += item.vx; item.y += item.vy;
if (item.x - item.radius < 0 || item.x + item.radius > canvas.width) item.vx *= -1;
if (item.y - item.radius < 0 || item.y + item.radius > canvas.height) item.vy *= -1;
if (Math.hypot(item.x - mouse.x, item.y - mouse.y) <= mouse.radius + item.radius) {
if (item.type === 'white') { score += 100; Sound.coin(); addFloatingText("+100", item.x, item.y, "#fff"); createSparks(item.x, item.y, `rgba(255,255,255,`, 8);
} else if (item.type === 'blue') { score += 100; slowTimer = 360; Sound.slow(); addFloatingText("SLOW DOWN!", item.x, item.y, "#3399ff"); createSparks(item.x, item.y, `rgba(51,153,255,`, 12);
} else if (item.type === 'gold') { score += 500; Sound.coin(); addFloatingText("+500!", item.x, item.y, "#ffd700"); createSparks(item.x, item.y, `rgba(255,215,0,`, 15);
} else if (item.type === 'rainbow') { score += 500; invincibleTimer = 300; Sound.invincible(); addFloatingText("INVINCIBLE!!", item.x, item.y, "#ff33cc"); createSparks(item.x, item.y, `rgba(255,51,204,`, 20); }
items.splice(i, 1);
}
}
if (!stampede.active && stampede.warning === 0 && Math.random() < 0.002) { stampede.warning = 70; warningText.style.opacity = 1; initStampedeBoars(); }
if (stampede.warning > 0) {
if (stampede.warning % 15 === 0) Sound.warning();
stampede.warning--;
if (stampede.warning <= 0) { stampede.active = true; stampede.x = -200; stampede.hasHit = false; warningText.style.opacity = 0; }
}
if (stampede.active) {
stampede.x += stampede.speed;
if (frames % 4 === 0) Sound.stampedeRumble();
if (!stampede.hasHit && target.x < stampede.x + 60 && target.x > stampede.x - 60) {
if (invincibleTimer > 0) {
// 無敵時はセーフ
} else if (!isSafe) {
stampede.hasHit = true; health -= 30; shakeTime = 20; Sound.crash();
createSparks(target.x, target.y, `rgba(139, 69, 19, `, 20); addFloatingText("STAMPEDED!!", target.x, target.y - 30, "#ff3366"); updateHealthBar();
} else {
createSparks(target.x, target.y, `rgba(100, 200, 255, `, 3);
if (frames % 10 === 0) Sound.guard();
}
}
if (stampede.x > canvas.width + 300) stampede.active = false;
}
if (health <= 0) {
gameState = 'gameover'; Sound.crash();
messageBox.style.display = 'flex'; finalScoreEl.innerText = `Score: ${Math.floor(score)}`; canvas.style.cursor = 'default';
}
scoreEl.innerText = `Score: ${Math.floor(score)}`;
particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.life -= p.decay; }); particles = particles.filter(p => p.life > 0);
texts.forEach(t => { t.y -= 1; t.life -= 0.02; }); texts = texts.filter(t => t.life > 0);
}
function draw() {
ctx.save();
if (shakeTime > 0) { ctx.translate((Math.random() - 0.5) * 15, (Math.random() - 0.5) * 15); shakeTime--; }
// CSSの木漏れ日背景を活かすため、クリアして透明にする
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (stampede.active) stampede.boars.forEach((b, i) => drawBoar(stampede.x + b.offsetX, b.y, b.size, i));
else if (stampede.warning > 0 && Math.floor(frames / 5) % 2 === 0) { ctx.fillStyle = 'rgba(255, 100, 50, 0.2)'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
if (safeFrames > 0 && invincibleTimer === 0) {
ctx.save();
ctx.beginPath(); ctx.arc(mouse.x, mouse.y, mouse.radius + 8, -Math.PI / 2, -Math.PI / 2 + (Math.PI * 2 * (safeFrames / MAX_SAFE_FRAMES)));
ctx.strokeStyle = '#00ff88'; ctx.lineWidth = 4;
ctx.shadowColor = '#000'; ctx.shadowBlur = 4; // 背景が明るいのでゲージにも影を
ctx.stroke();
ctx.restore();
}
ctx.beginPath(); ctx.arc(mouse.x, mouse.y, mouse.radius, 0, Math.PI * 2);
ctx.strokeStyle = invincibleTimer > 0 ? `hsl(${frames * 5 % 360}, 100%, 60%)` : (shakeTime > 0 ? '#ff3366' : '#2e7d32'); ctx.lineWidth = invincibleTimer > 0 ? 4 : 3; ctx.stroke();
ctx.fillStyle = invincibleTimer > 0 ? `hsla(${frames * 5 % 360}, 100%, 60%, 0.15)` : (shakeTime > 0 ? 'rgba(255, 51, 102, 0.3)' : 'rgba(255, 255, 255, 0.25)'); ctx.fill();
items.forEach(item => {
ctx.save(); ctx.beginPath(); ctx.arc(item.x, item.y, item.radius, 0, Math.PI * 2);
if (item.type === 'white') { ctx.fillStyle = '#ffffff'; ctx.shadowColor = '#000'; }
else if (item.type === 'blue') { ctx.fillStyle = '#3399ff'; ctx.shadowColor = '#000'; }
else if (item.type === 'gold') { ctx.fillStyle = '#ffd700'; ctx.shadowColor = '#000'; }
else if (item.type === 'rainbow') { ctx.fillStyle = `hsl(${frames * 8 % 360}, 100%, 60%)`; ctx.shadowColor = '#000'; }
ctx.shadowBlur = 8; ctx.fill(); ctx.restore();
});
ctx.save(); ctx.translate(target.x, target.y); ctx.rotate(target.angle + Math.PI / 2);
ctx.beginPath(); ctx.arc(0, 0, target.radius, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
// 背景が明るいので、無敵じゃない時は黒っぽい影を落として見やすくする
ctx.shadowBlur = invincibleTimer > 0 ? 25 : 6;
ctx.shadowColor = invincibleTimer > 0 ? `hsl(${frames * 10 % 360}, 100%, 50%)` : 'rgba(0,0,0,0.4)';
ctx.fill(); ctx.shadowBlur = 0;
if (slowTimer > 0) { ctx.beginPath(); ctx.arc(0, 0, target.radius, 0, Math.PI * 2); ctx.fillStyle = 'rgba(51, 153, 255, 0.4)'; ctx.fill(); }
ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(-6, -4, 2.5, 0, Math.PI * 2); ctx.arc(6, -4, 2.5, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = '#111'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-10, -9); ctx.lineTo(-4, -7); ctx.moveTo(10, -9); ctx.lineTo(4, -7); ctx.stroke();
ctx.beginPath(); ctx.arc(0, 3, 5, 0, Math.PI); ctx.stroke();
ctx.restore();
particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, Math.random() * 3 + 1, 0, Math.PI * 2); ctx.fillStyle = `${p.color}${p.life})`; ctx.fill(); });
texts.forEach(t => {
ctx.fillStyle = t.color; ctx.globalAlpha = t.life; ctx.font = "bold 18px Arial"; ctx.textAlign = "center";
ctx.shadowColor = '#000'; ctx.shadowBlur = 4; // 文字にも影をつけて読みやすく
ctx.fillText(t.text, t.x, t.y);
ctx.shadowBlur = 0; ctx.globalAlpha = 1.0;
});
ctx.restore();
}
function loop() {
if (gameState === 'playing') { update(); draw(); requestAnimationFrame(loop); }
}
</script>
</body>
</html>


■ 今日の学び

実は今回、この「100のゲーム実験室」にとって、
記念すべき 100個目の記事 になります!( ⁎ᵕᴗᵕ⁎ )✨

そして、その100個目を飾るゲームは……
なんと、ナンバリング「#2」にあたる『丸太郎と突撃猪』です!

初期の頃に作った前作「#2」とはまったく違うゲームになりましたが、
いろんな要素がうまくカチッとハマって、
前作に負けない “手に汗握る” ゲームに仕上がりました。┗(`・ω・´)┛フンス

何より驚いたのは、丸太郎の生意気なお顔や、
ドドドッと押し寄せるイノシシの大群など、 見た目も動きも、

AIと一緒にゲームを作り始めた頃と比べて
すごく良くなっていることです。( Ꙭ)‼︎

振り返ってみると、オリジナルの「#2」を作ったのは今年の1月26日。
そして今回の新しい丸太郎を生み出したのは6月16日でした。

この数ヶ月の間に、AI自身が賢く成長したのか、
それとも私の「AIへの相談のしかた」が上手になったのか……。

( ˘ω˘ )。oO(きっと、両方ですね!)

どちらにしても、自分の思い描いた
「いい感じの仕上がり」に近づけられるようになってきて、
今、ゲーム作りがとっても楽しいです!٩( ‘ω’ )و

今回の『丸太郎と突撃猪』は、
そんな私たちの実験室の変化と成長を感じるのにぴったりの、
記念すべき100個目のゲームになったと思います。

これからも、ドタバタ楽しく実験を続けていきますので、
引き続きよろしくお願いします!(っ‘ω’c)ワクワク

■ 次回は、101個目のゲーム

「100のゲーム実験室」の原点に戻ります。

この実験室がスタートしたのは R8年1月22日
AIとの最初の一歩が、毎日1つゲームを作るきっかけになりました。

そこから約5か月と少し。
一つ一つのゲームはまだまだ不完全ですが、
7月4日現在で作ったゲームは 170 を超えました!

不完全でもいい、発展途上でもいい。
その積み重ねが、この実験室の“美学”になってきた気がします。(*^v^)

次回の 101 個目は、そんな原点を思い出すようなゲームです。

ぜひ楽しみにしていてくださいね。(*^v^)ノシ

次の実験はこちら
(発展途上と、不完全の美学、AIと考える100ゲーム実験室
「歩いて宙返りするパンダ」



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

~このブログについて~

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

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


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

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

続きを読む