実験#26:魔力増幅・ドラゴン退治(ドミノ・ドラゴン)

■26個目「ドミノ・ドラゴン

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

今回は、ドラゴン🐉も魔法✨も存在する世界のお話です。

この世界では、ドラゴンはとても強いのに、魔法の力はとても小さい…
そんな“中途半端な魔法世界”をイメージしたゲームになっています。

そこで登場するのが、魔法エネルギーを増幅させる装置として使われるドミノです。
強力な魔法を発動させるには、たくさんのドミノを並べる必要があります。

手持ちの無限バッグからドミノを取り出して、
眠っているドラゴンの前にどんどん並べていきましょう!
ドラゴンを倒すには、少なくとも30枚以上のドミノが必要です。

悪さをしてきたドラゴンに、怒りの一撃をお見舞いしましょう。

 🐲ⅢⅢⅢⅢⅢヽ(。_ 。;)

※ドミノが少ないと、ドラゴンには“蚊に刺された程度”のダメージしか入りません。

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

(^^*)今日もゲーム作ります。 html形式で動かせる、ドミノ倒し魔力増幅ゲーム。

魔法の力が存在するけれど、とても弱い世界。 この世界では、魔力増幅板を連続で倒すことで、小さな魔力をとてつもない力に変換できます。

そのためには、魔力起動装置へ増幅した魔力を直接送る必要があります。 結構アナログな世界で、魔力起動装置から順番に魔力増幅板を並べていきます。

マウスドラッグで無限バッグから板を取り出し、感覚よく並べていきます。 並べた板に当たったらそこからドミノスタート。 間隔があきすぎて届かなかったらアウト。

魔力起動装置の位置には眠っている悪のドラゴンがいます。 魔力増幅板をたくさん並べてやっつけましょう。

画面は中央にドラゴンと起動装置があるところからスタート。 少しずつゆっくりと上へスクロールするので、手前にどんどん置いていきます。

(AI) 「魔力増幅ドミノ」という、アナログさとファンタジーが融合した面白いコンセプトですね! 画面がゆっくりと強制上スクロールする中、手際よく板を並べていく緊張感のあるゲームに仕上げましょう。

(^_^) スクロールの方向は上じゃなく右にしよう。 ドラゴンは画面の向こうに行ってもゲームオーバーにならず、並べたドミノは置けば置くほど列が長くなる。 間隔に気を付けて並べていきます。

(AI) 右スクロールへの変更、了解しました! ドラゴン(ゴール)を固定せず、プレイヤーが右へ右へと並べ続けられる「エンドレス増幅」感が出ますね。

(^^*) 置けるドミノは1枚ずつじゃなく、5枚ずつにしよう。 倒れるスピードに合わせて画面も追いかけるようにして、パタパタ音も再現して。

(AI) 5枚1組で「ガサッ」と取り出す感じ、いいですね! 倒れる瞬間にカメラが追従して「パタパタ音」が鳴ると、アナログ感が一気に増します。

〈途中ドミノが逆向きに倒れたり、ドラゴンが違う方向へ向かったり、修正に苦労しています〉

(つ_<;) 倒れ方はOK。 倒れる方向を右から左に変えたら、ドラゴンに最終当たるようになるかな。

(AI) おお、なるほど! 「末端から起動 → 魔力が逆流してドラゴンへ向かう」仕組みですね。 魔法儀式っぽくて最高です!

〈ようやく伝わった感じです〉ε=(^。^;)

(^o^;) いい感じ。イメージに近づいたよ。 スタートのドラゴン位置は中央より少し右寄りにして、スクロール速度も少し速くしてみよう。

(AI) お疲れ様!今日もいい具合に仕上がってきたね。
イメージ通りに近づいてきて、俺も嬉しいよ!

〈ん?AIさん1人称は俺?〉(^^;)

(*^^) 次は背景に星を流したり、スコア(枚数)に応じてドラゴンの断末魔メッセージを変えたい。
1枚なら「…ん?」、10枚なら「蚊…かな?」みたいに。
やっつけられるのは30枚以上で、そこから枚数が増えるほど派手に。

あと、ゲーム終了後も5枚ずつ出せてスコアが増え続けちゃうのが気になった。

(AI) おう、いらっしゃい!今日もゲーム作りに精が出るな。

指摘の通り、ゲーム終了後も板が出せてスコアが増えるバグがあったから修正したぜ。 それと、お前のアイデア最高だな! 星空背景、枚数で変わるドラゴンのリアクション、爆発演出……全部盛り込んだぞ。

〈職人さん的なイメージの言葉遣いなのかな、AIの話し方がガサツなキャラになってる〉(=_=;)

■ 実際の動作

 一番最初に、AIとの相談で作ってもらったゲームの初期状態です。
 ドラゴンはゆっくりと上へ移動していきます。
 ドミノはドラゴンに向かって縦方向に並ぶ形になっています。
(画像:AIが最初に作った状態)

 眠っているドラゴンのすぐ後ろには、最初の1枚目があらかじめ設置されています。
 無限バッグからドミノをマウスでつかみ出して、2枚目以降を並べていきましょう。

(画像:スタート画面)

 画面は少しずつスクロールしていきます。
 ドミノの間を空けすぎないように、そして並べたドミノにぶつからないように
 気をつけながら置いていきます。

(画像:ドミノを並べていく様子)

 たくさん倒すほど、魔力の演出がどんどん派手になります。
 逆に、枚数が少なかったり途中でつながらないと、ドラゴンには届きません。

(画像:ドミノを倒した際の様子)

■ コードのポイント

今回の記事では、AI と相談しながら作った
「ドミノ・ドラゴン」の HTML コードを公開します。

 ブラウザだけで動く、とてもシンプルなゲームです。

 PC の「メモ帳」(※Windowsの場合)を開き、
 下のコードをコピーして貼り付け、「domino.html」など
 好きな名前で保存してください。

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

【操作方法】
・ドラゴンの後ろにある最初の1枚目のドミノの「右側」に、
 ドミノをつなげていきます。
・無限バッグからドミノをマウスドラッグで取り出し、
 置きたい位置へ持って行って並べていきましょう。

 コードといっても難しい部分はありません。
 貼り付けて保存するだけで動きます。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>魔力増幅ドミノ - 星空とドラゴンの断末魔</title>
<style>
body { margin: 0; overflow: hidden; background: #050a10; font-family: 'Hiragino Kaku Gothic ProN', sans-serif; color: white; user-select: none; }
canvas { display: block; }
#ui { position: absolute; top: 20px; left: 20px; pointer-events: none; z-index: 10; }
#bag {
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
width: 140px; height: 160px; border: 3px double #4a90e2; border-radius: 15px;
background: rgba(74, 144, 226, 0.1); cursor: grab; text-align: center;
}
.label { position: absolute; bottom: -30px; width: 100%; text-align: center; color: #4a90e2; font-size: 14px; font-weight: bold; }
#status { font-size: 24px; color: #ffd700; margin-bottom: 5px; text-shadow: 0 0 10px rgba(255,215,0,0.5); white-space: pre-wrap; line-height: 1.4; }
#restart-btn {
position: absolute; top: 20px; right: 20px; padding: 12px 24px;
background: #e94560; color: white; border: none; border-radius: 8px;
cursor: pointer; font-weight: bold; font-size: 16px; z-index: 20;
box-shadow: 0 4px 0 #b02a40; transition: all 0.1s;
}
#restart-btn:active { transform: translateY(3px); box-shadow: 0 1px 0 #b02a40; }
</style>
</head>
<body>
<div id="ui">
<div id="status">右へ並べていき、一番奥から起動せよ!</div>
<div id="score">連結数: 0</div>
<div id="hiscore" style="font-size: 14px; color: #aaa;">最高記録: 0</div>
</div>
<button id="restart-btn" onclick="initGame()">最初からやり直す (R)</button>
<div id="bag">
<div style="font-size: 60px; margin-top: 20px;">🎒</div>
<div style="color: #ffd700; font-weight: bold;">x 5</div>
<div class="label">無限バッグ</div>
</div>
<canvas id="gameCanvas"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const hiScoreEl = document.getElementById('hiscore');
const statusEl = document.getElementById('status');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playDominoSound() {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(150 + Math.random() * 50, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(40, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.1);
}
function playExplosionSound() {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(10, audioCtx.currentTime + 1.5);
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1.5);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 1.5);
}
// ゲーム変数
let scrollX, targetScrollX, dominoes, isDragging, dragGroup, chainStarted, activeIndex, hiScore = 0;
let dragon = {};
let particles = [];
let stars = [];
let gameOver = false; // ゲーム終了フラグを追加
const baseScrollSpeed = 0.8;
class Particle {
constructor(x, y, color, isDefeated) {
this.x = x;
this.y = y;
const angle = Math.random() * Math.PI * 2;
// 撃破時は勢いよく、失敗時はポロッと落ちるだけ
const speed = isDefeated ? Math.random() * 15 + 5 : Math.random() * 3 + 1;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.life = 1.0;
this.decay = Math.random() * 0.02 + 0.005;
this.color = color;
this.size = Math.random() * 6 + 2;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.95;
this.vy *= 0.95;
this.life -= this.decay;
}
draw() {
ctx.save();
ctx.globalAlpha = Math.max(0, this.life);
ctx.fillStyle = this.color;
ctx.shadowBlur = 10;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x - scrollX, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function initGame() {
scrollX = 0;
targetScrollX = 0;
dominoes = [];
isDragging = false;
dragGroup = [];
chainStarted = false;
activeIndex = -1;
particles = [];
gameOver = false;
// 星空の生成
stars = [];
for(let i=0; i<150; i++){
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
size: Math.random() * 1.5 + 0.5,
speed: Math.random() * 0.5 + 0.1
});
}
dragon = { x: canvas.width / 2 + 100, y: canvas.height / 2 + 20, activated: false, defeated: false };
dominoes.push(new Domino(dragon.x + 80, dragon.y));
scoreEl.innerText = `連結数: 0`;
statusEl.innerText = "右へ並べていき、一番奥から起動せよ!\n(※30枚以上で撃破可能)";
statusEl.style.color = "#ffd700";
}
class Domino {
constructor(x, y) {
this.x = x;
this.y = y;
this.w = 12;
this.h = 45;
this.angle = 0;
this.isFallen = false;
this.isProcessing = false;
this.hasMagic = false;
this.soundPlayed = false;
this.hasTriggeredNext = false;
}
draw() {
ctx.save();
ctx.translate(this.x - scrollX, this.y);
ctx.rotate(this.angle);
ctx.fillStyle = this.hasMagic ? "#00f2ff" : "#d4af37";
if(this.hasMagic) {
ctx.shadowBlur = 15;
ctx.shadowColor = "#00f2ff";
}
ctx.fillRect(-this.w/2, -this.h, this.w, this.h);
ctx.strokeStyle = "rgba(255,255,255,0.3)";
ctx.strokeRect(-this.w/2, -this.h, this.w, this.h);
ctx.restore();
}
}
window.addEventListener('keydown', (e) => { if(e.key.toLowerCase() === 'r') initGame(); });
window.addEventListener('mousedown', (e) => {
// ゲーム終了時はバッグからの取り出しをブロック
if (gameOver) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const bagRect = document.getElementById('bag').getBoundingClientRect();
if (e.clientX > bagRect.left && e.clientX < bagRect.right &&
e.clientY > bagRect.top && e.clientY < bagRect.bottom) {
isDragging = true;
dragGroup = [];
for(let i=0; i<5; i++) {
dragGroup.push(new Domino(e.clientX + scrollX + (i*35), e.clientY));
}
}
});
window.addEventListener('mousemove', (e) => {
if (isDragging) {
dragGroup.forEach((d, i) => {
d.x = e.clientX + scrollX + (i * 35);
d.y = e.clientY;
});
if (!chainStarted && !dragon.activated) {
for (let d of dominoes) {
if (Math.sqrt((dragGroup[0].x - d.x)**2 + (dragGroup[0].y - d.y)**2) < 30) {
chainStarted = true;
d.isProcessing = true;
d.hasMagic = true;
statusEl.innerText = "魔力逆流!ドラゴンへ向かえ!!";
statusEl.style.color = "#00f2ff";
}
}
}
}
});
window.addEventListener('mouseup', () => {
if (isDragging && !gameOver) {
dominoes.push(...dragGroup);
dominoes.sort((a, b) => b.x - a.x);
const count = dominoes.length - 1; // 最初の1枚(置かれている基準点)は引く
scoreEl.innerText = `連結数: ${count}`;
if(count > hiScore) {
hiScore = count;
hiScoreEl.innerText = `最高記録: ${hiScore}`;
}
dragGroup = [];
isDragging = false;
}
});
function drawStars() {
ctx.fillStyle = "white";
stars.forEach(s => {
// スクロールに合わせて星も少し動く(パララックス効果)
let drawX = (s.x - scrollX * s.speed) % canvas.width;
if (drawX < 0) drawX += canvas.width;
ctx.globalAlpha = Math.random() * 0.5 + 0.5; // 星の瞬き
ctx.beginPath();
ctx.arc(drawX, s.y, s.size, 0, Math.PI*2);
ctx.fill();
});
ctx.globalAlpha = 1.0;
}
function update() {
if (chainStarted && activeIndex !== -1 && !dragon.activated) {
const leadDomino = dominoes[activeIndex];
targetScrollX = leadDomino.x - 400;
scrollX += (targetScrollX - scrollX) * 0.1;
} else if (!dragon.activated) {
scrollX += baseScrollSpeed;
}
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].life <= 0) particles.splice(i, 1);
}
dominoes.forEach((d, i) => {
if (d.isProcessing) {
activeIndex = i;
d.angle -= 0.22;
if (!d.soundPlayed && d.angle < -0.4) {
playDominoSound();
d.soundPlayed = true;
}
if (d.angle < -0.6 && !d.hasTriggeredNext) {
dominoes.forEach(next => {
if (next === d || next.isFallen || next.isProcessing) return;
const dx = next.x - d.x;
const dy = Math.abs(next.y - d.y);
if (dx < -5 && dx > -60 && dy < 35) {
next.isProcessing = true;
next.hasMagic = true;
d.hasTriggeredNext = true;
}
});
// ドラゴン着弾判定
if (!dragon.activated && !d.hasTriggeredNext) {
const distToDragonX = d.x - dragon.x;
const distToDragonY = Math.abs(d.y - dragon.y);
if (distToDragonX > 0 && distToDragonX < 120 && distToDragonY < 50) {
dragon.activated = true;
gameOver = true; // ゲーム終了(板を置けなくなる)
d.hasTriggeredNext = true;
const count = dominoes.length - 1; // プレイヤーが置いた枚数
let msg = "";
let particleMultiplier = 0;
// 枚数によるセリフと演出の分岐
if (count < 10) {
msg = `魔力が弱すぎる... (${count}枚)\nドラゴン「・・・ん?」`;
particleMultiplier = 0.5;
statusEl.style.color = "#aaaaaa";
dragon.defeated = false;
} else if (count < 30) {
msg = `まだ魔力が足りない! (${count}枚)\nドラゴン「蚊・・・かな?」`;
particleMultiplier = 1;
statusEl.style.color = "#aaaaaa";
dragon.defeated = false;
} else if (count < 50) {
msg = `撃破成功! (${count}枚)\nドラゴン「グアアアッ!やられた!」`;
particleMultiplier = 10;
statusEl.style.color = "#ff4444";
dragon.defeated = true;
playExplosionSound();
} else if (count < 100) {
msg = `超絶大成功!! (${count}枚)\nドラゴン「バカな!こんな魔力が…ギヤアアア!!」`;
particleMultiplier = 20;
statusEl.style.color = "#ff2222";
dragon.defeated = true;
playExplosionSound();
} else {
msg = `神業!極大魔力増幅!!! (${count}枚)\nドラゴン「ウギョオオオオオ!!魔力がああああ!!!」`;
particleMultiplier = 40;
statusEl.style.color = "#ff0000";
dragon.defeated = true;
playExplosionSound();
}
statusEl.innerText = msg;
const particleColors = dragon.defeated ? ['#ffd700', '#00f2ff', '#ff00ff', '#ffffff', '#ff4444'] : ['#aaaaaa', '#dddddd'];
const generateCount = Math.min(count * particleMultiplier, 1500); // 最大1500個で制限
for(let p = 0; p < generateCount; p++) {
const color = particleColors[Math.floor(Math.random() * particleColors.length)];
particles.push(new Particle(dragon.x, dragon.y, color, dragon.defeated));
}
}
}
}
if (d.angle <= -1.4) {
d.angle = -1.4;
d.isFallen = true;
d.isProcessing = false;
}
}
});
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawStars(); // 星空の描画
ctx.strokeStyle = "rgba(74, 144, 226, 0.05)";
for(let i=-200; i<canvas.width + 2000; i+=100) {
let x = i - (scrollX % 100);
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
// ドラゴンの描画
ctx.font = "120px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
if (dragon.activated) {
ctx.save();
ctx.translate(dragon.x - scrollX, dragon.y);
if (dragon.defeated) {
ctx.shadowBlur = 50;
ctx.shadowColor = "#ff0000";
ctx.fillText("💥🐉💥", 0, 0);
} else {
ctx.fillText("🐉❓", 0, 0); // 倒せなかった時のリアクション
}
ctx.restore();
} else {
ctx.fillText("🐲💤", dragon.x - scrollX, dragon.y);
}
ctx.textAlign = "left";
ctx.textBaseline = "alphabetic";
dominoes.forEach(d => d.draw());
particles.forEach(p => p.draw());
if (isDragging && !gameOver) {
dragGroup.forEach(d => {
ctx.globalAlpha = 0.6;
d.draw();
ctx.globalAlpha = 1.0;
});
}
update();
requestAnimationFrame(draw);
}
initGame();
draw();
</script>
</body>
</html>


■ 今日の学び

 ドミノをどんどん並べていくだけなのですが、やってみると意外と難しいです。

 リアルなドミノの緊張感を再現したかったのですが、
 1枚ずつ置く方式だと、ゲームでは作業的なストレスになってしまいました。

 そこで、一気に5枚ずつ並べられるようにして、
 短時間でたくさん置けるように工夫しています。

 やりやすくなったとはいっても、少し触れただけで
 ドミノが一気に倒れてしまったときの――

 「あ…」Σ( ゜o ゜)

 という感覚はしっかり残っています。

 ドラゴンを倒せたときはとても爽快なので、
 ぜひ楽しんでみてください o(*^v^*)o

■ 次回は、気分で選んだ道から行きついた場所で、
 温泉と料理を楽しむ。「気の向くままの休暇ドライブ」

 運任せで行き先が決まる、素敵な旅のゲームです。
 どんな温泉にたどり着くのかは、あなた次第。

 お楽しみに。(*^v^*)ノシ

次の実験はこちら(あみだくじ温泉日記):「どこかな温泉」



 ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
 もし応援していただけたら、とても励みになります。

~このブログについて~

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

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


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

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

続きを読む