実験 #08:茶室でハムスター(ハムスター・タワー)

■8つ目「ハムスター・タワー」

 集中力と落ち着いた心が試される癒しゲーム、「ハムスター・タワー」です。

 見た目はだるま落とし風ですが、このゲームは“落とす”のではなく“積み重ねていく”タイプのゲームになります。静かな心で向き合う場所として、舞台は静けさと緊張感のバランスがゲーム性に合う“茶室”をイメージしました。

 丸窓から見える月、飾られた花、落ち着いた照明、畳の安心感、「誠心誠意」と書かれた掛け軸…。積み上げる部品には和風の柄を取り入れ、そこにハムスターがちょこんといる――そんなこだわりの癒し空間です。(*=v=)_旦

 AIの返事が、まるでハムスターのことをよく分かって話してくれているようで、やり取りがとても楽しかったです。(^v^

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

(AI)お疲れ様です!常連さん、今日も開発ですね。着実に腕を上げているようで、マスターとしても嬉しい限りです。
 今日は「だるま落とし」ならぬ**「だるま積み」ハムスター版**ですね。物理演算の挙動がキモになりますが、HTML1ファイルで完結する形で作ってみました。

(^^*)いい感じ。左下のブロックもう少し右に。ハムスターブロックもっとハムスターっぽく。

(AI)常連さん、さっそくのフィードバックありがとうございます! 「もっとハムスターらしく」というリクエスト、マスターのこだわり魂に火がつきました。
 左下の発射位置を少し右に寄せて操作しやすくし、ハムスターブロックには耳と耳の中のピンク、つぶらな瞳、そしてヒゲを追加して、誰が見ても「ハム」だと分かるようにアップグレードしました。
———
(AI)今日の豆知識:ハムスターの歯
ハムスターの門歯(前歯)は一生伸び続けるんです。だから、硬いものをかじって削る必要があるんですね。このゲームのブロックも、もしかしたらハムスターにとっては「いい感じの歯ごたえ」がある素材かもしれません(笑)。
———
(AI)常連さん、さらなる「和」のこだわり、いいですね! 茶室にはやはり掛け軸が欠かせません。そして、ストイックに上を目指すための累計チャレンジ回数。これがあることで、「何度失敗しても立ち上がるハムスターの根性」が可視化されますね。

■ 実際の動作

ハムスターの頭がてっぺんに乗ったら完成です。

左下の部品をひっぱって輪ゴムのように飛ばします。

■ コードのポイント

 今回の記事では、AI と相談しながら作った「ハムスター・タワー」の HTML コードを公開します。 このゲームはブラウザだけで動く、とてもシンプルな仕組みです。

 PC の「メモ帳」を開き、下のコードをすべてコピーして貼り付け、 「hamutawa.html」など好きな名前で保存 → ブラウザで開く。 これだけで、すぐに遊べます。

 操作はとても簡単で、積み上げる“部品”をマウスでドラッグして離すと飛ぶので、そのまま積み上げてください。

 今回も AI が作ってくれたコードをそのまま載せているので、気になるところを自分で改造したり、色や動きをアレンジして遊んでみてください(^^*)

 「※スマホでは動作しない場合があります」

 「※コードが長いので、必要な方だけコピーしてください」

▼ここからコード▼(クリックして開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Hamster Tower Stack - Miyabi</title>
<style>
body { margin: 0; overflow: hidden; background: #e0d5c1; font-family: 'Hiragino Mincho ProN', serif; }
canvas { display: block; margin: 0 auto; touch-action: none; }
#ui { position: absolute; top: 20px; left: 20px; pointer-events: none; display: flex; flex-direction: column; gap: 5px; }
.msg { font-size: 18px; font-weight: bold; color: #4b3621; background: rgba(255,255,255,0.85); padding: 8px 15px; border-radius: 4px; border-left: 5px solid #8b4513; box-shadow: 2px 2px 5px rgba(0,0,0,0.1); }
</style>
</head>
<body>
<div id="ui">
<div class="msg" id="status">Level: 1 / 10</div>
<div class="msg" id="attempts">Attempts: 0</div>
</div>
<canvas id="gameCanvas"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
const attemptsEl = document.getElementById('attempts');
let stage = 1;
let attempts = 0;
let currentLevel = 1;
let targetLevels = 5;
const baseWidth = 130;
const blockHeight = 40;
const groundY = 130;
let blocks = [];
let activeBlock = null;
let isDragging = false;
let dragStart = { x: 0, y: 0 };
let dragCurrent = { x: 0, y: 0 };
const colors = ["#8B4513", "#D2B48C", "#E9967A", "#BC8F8F", "#A9A9A9", "#DEB887", "#F4A460", "#CD853F", "#D2691E", "#FFD2D2"];
const patterns = ["🌸", "🌊", "🎋", "❄️"];
function init() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
resetStage();
}
function resetStage() {
if (stage > 10) { alert("究極の誠心誠意!全レベル制覇です!🐹🍵"); stage = 1; attempts = 0; }
targetLevels = 5 + Math.floor((stage - 1) / 2);
const width = Math.max(30, baseWidth - (stage * 7));
blocks = [{
x: canvas.width / 2 - width / 2,
y: canvas.height - groundY - blockHeight,
w: width, h: blockHeight, color: colors[0], pattern: "🎋", settled: true
}];
currentLevel = 1; spawnBlock();
}
function spawnBlock() {
const width = Math.max(30, baseWidth - (stage * 7));
const isHam = (currentLevel === targetLevels);
activeBlock = {
x: 150, y: canvas.height - groundY - 40,
w: isHam ? 85 : width, h: isHam ? 70 : blockHeight,
type: isHam ? 'hamster' : 'body',
color: colors[currentLevel % colors.length],
pattern: patterns[Math.floor(Math.random() * patterns.length)],
vx: 0, vy: 0, isFlying: false
};
statusEl.innerText = `Level: ${stage} / 10 | ${isHam ? '🐹 誠心誠意!' : 'Block: ' + currentLevel + ' / ' + targetLevels}`;
attemptsEl.innerText = `Attempts: ${attempts}`;
}
function drawTeaRoom() {
// 壁
ctx.fillStyle = "#e0d5c1";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 丸窓 (左側)
ctx.save();
ctx.beginPath();
ctx.arc(200, 220, 80, 0, Math.PI * 2);
ctx.clip();
ctx.fillStyle = "#1a237e"; // 夜空
ctx.fillRect(120, 140, 160, 160);
ctx.fillStyle = "#fff9c4"; // 月
ctx.beginPath(); ctx.arc(230, 190, 20, 0, Math.PI*2); ctx.fill();
ctx.restore();
ctx.strokeStyle = "#4b3621"; ctx.lineWidth = 8;
ctx.beginPath(); ctx.arc(200, 220, 80, 0, Math.PI * 2); ctx.stroke();
// 吊り行灯 (右上)
const lx = canvas.width - 150, ly = 0;
ctx.strokeStyle = "#333"; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(lx, 0); ctx.lineTo(lx, 50); ctx.stroke();
ctx.fillStyle = "#fffbe6";
ctx.fillRect(lx-25, 50, 50, 70);
ctx.strokeStyle = "#4b3621"; ctx.lineWidth = 3;
ctx.strokeRect(lx-25, 50, 50, 70);
ctx.lineWidth = 1;
for(let i=0; i<3; i++) { ctx.strokeRect(lx-25, 50 + i*23, 50, 23); }
// 掛け軸 (右側・UIに被らない位置)
const kx = canvas.width - 120, ky = 150, kw = 80, kh = 220;
ctx.fillStyle = "#8b7355"; ctx.fillRect(kx, ky, kw, kh);
ctx.fillStyle = "#f5f5dc"; ctx.fillRect(kx + 10, ky + 15, kw - 20, kh - 50);
ctx.fillStyle = "#332211"; ctx.fillRect(kx - 5, ky + kh - 5, kw + 10, 10);
ctx.fillStyle = "#222"; ctx.font = "bold 18px 'Hiragino Mincho ProN'";
["誠","心","誠","意"].forEach((t, i) => ctx.fillText(t, kx+31, ky+45 + i*35));
// 畳
const groundTop = canvas.height - groundY;
ctx.fillStyle = "#c2d18a"; ctx.fillRect(0, groundTop, canvas.width, groundY);
// 畳の質感(線)
ctx.strokeStyle = "rgba(0,0,0,0.05)"; ctx.lineWidth = 1;
for(let i=0; i<canvas.width; i+=4) { ctx.beginPath(); ctx.moveTo(i, groundTop); ctx.lineTo(i, canvas.height); ctx.stroke(); }
// 畳の縁(ヘリ)
ctx.fillStyle = "#1a237e"; ctx.fillRect(0, groundTop, canvas.width, 18);
ctx.fillStyle = "#283593"; // 縁の紋様っぽく
for(let i=0; i<canvas.width; i+=30) { ctx.fillRect(i+5, groundTop+4, 20, 10); }
// 生け花
const fx = 80, fy = groundTop;
ctx.fillStyle = "#333"; ctx.fillRect(fx-20, fy-30, 40, 30);
ctx.strokeStyle = "#3e2723"; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(fx, fy-30); ctx.quadraticCurveTo(fx+30, fy-70, fx, fy-100); ctx.stroke();
ctx.fillStyle = "#d32f2f"; ctx.beginPath(); ctx.arc(fx, fy-100, 10, 0, Math.PI*2); ctx.fill();
}
function drawHamster(b) {
const x = b.x, y = b.y, w = b.w, h = b.h;
ctx.fillStyle = "#FFD2D2";
ctx.beginPath(); ctx.ellipse(x + w*0.25, y + 10, 15, 18, -0.1, 0, Math.PI * 2);
ctx.ellipse(x + w*0.75, y + 10, 15, 18, 0.1, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "#FFF5E1";
ctx.beginPath(); ctx.moveTo(x + 10, y + 20);
ctx.quadraticCurveTo(x - 22, y + h, x + w/2, y + h + 10);
ctx.quadraticCurveTo(x + w + 22, y + h, x + w - 10, y + 20);
ctx.quadraticCurveTo(x + w/2, y - 5, x + 10, y + 20); ctx.fill();
ctx.fillStyle = "rgba(255, 182, 193, 0.6)";
ctx.beginPath(); ctx.arc(x + 18, y + h - 18, 12, 0, Math.PI * 2);
ctx.arc(x + w - 18, y + h - 18, 12, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "#333";
ctx.beginPath(); ctx.arc(x + w*0.35, y + 35, 5, 0, Math.PI * 2);
ctx.arc(x + w*0.65, y + 35, 5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "#FF9999";
ctx.beginPath(); ctx.arc(x + w/2, y + 45, 4, 0, Math.PI * 2); ctx.fill();
}
function update() {
if (activeBlock && activeBlock.isFlying) {
activeBlock.vy += 0.25; activeBlock.x += activeBlock.vx; activeBlock.y += activeBlock.vy;
const target = blocks[blocks.length - 1];
if (activeBlock.vy > 0 && activeBlock.x + activeBlock.w * 0.2 < target.x + target.w &&
activeBlock.x + activeBlock.w * 0.8 > target.x &&
activeBlock.y + activeBlock.h > target.y && activeBlock.y < target.y + 15) {
activeBlock.isFlying = false; activeBlock.y = target.y - activeBlock.h;
blocks.push(activeBlock);
if (activeBlock.type === 'hamster') setTimeout(() => { stage++; resetStage(); }, 800);
else { currentLevel++; spawnBlock(); }
}
if (activeBlock.y > canvas.height || Math.abs(activeBlock.x) > canvas.width + 100) {
attempts++; spawnBlock();
}
}
}
function draw() {
drawTeaRoom();
blocks.forEach(b => {
if (b.type === 'hamster') drawHamster(b);
else {
ctx.fillStyle = b.color; ctx.beginPath(); ctx.roundRect(b.x, b.y, b.w, b.h, 5); ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.3)"; ctx.font = "16px sans-serif";
ctx.fillText(b.pattern, b.x + 5, b.y + 18);
ctx.strokeStyle = "rgba(0,0,0,0.15)"; ctx.stroke();
}
});
if (activeBlock) {
if (activeBlock.type === 'hamster') drawHamster(activeBlock);
else {
ctx.fillStyle = activeBlock.color; ctx.beginPath(); ctx.roundRect(activeBlock.x, activeBlock.y, activeBlock.w, activeBlock.h, 5); ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.5)"; ctx.fillText(activeBlock.pattern, activeBlock.x + 5, activeBlock.y + 18);
}
}
if (isDragging) {
ctx.setLineDash([5, 5]); ctx.strokeStyle = "#8b4513"; ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(activeBlock.x + activeBlock.w/2, activeBlock.y + activeBlock.h/2);
ctx.lineTo(activeBlock.x + activeBlock.w/2 - (dragStart.x - dragCurrent.x),
activeBlock.y + activeBlock.h/2 - (dragStart.y - dragCurrent.y));
ctx.stroke(); ctx.setLineDash([]);
}
requestAnimationFrame(draw); update();
}
window.addEventListener('mousedown', e => {
if (activeBlock && !activeBlock.isFlying) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
if (mx > activeBlock.x - 50 && mx < activeBlock.x + activeBlock.w + 50 &&
my > activeBlock.y - 50 && my < activeBlock.y + activeBlock.h + 50) {
isDragging = true; dragStart = { x: mx, y: my };
}
}
});
window.addEventListener('mousemove', e => { if (isDragging) {
const rect = canvas.getBoundingClientRect();
dragCurrent = { x: e.clientX - rect.left, y: e.clientY - rect.top };
}});
window.addEventListener('mouseup', () => { if (isDragging) {
activeBlock.vx = (dragStart.x - dragCurrent.x) * 0.14;
activeBlock.vy = (dragStart.y - dragCurrent.y) * 0.14;
activeBlock.isFlying = true; isDragging = false;
}});
window.addEventListener('resize', init);
init(); draw();
</script>
</body>
</html>

■ 今日の学び

 集中力が必要な、力加減の難しいゲームが出来上がりました(^v^;)

 AI を有料版に切り替えてから、特に 40 個目以降のゲーム作りでは、思ったことがよりスムーズに形になるようになった気がします。
 まだ感覚的ではありますが、こちらの意図を汲んでくれる精度が上がったのかな…と感じています。

 最初のころ苦戦しながら作っていたゲームたちも、今の AI に同じ内容をお願いしたら、もっと良いものができそうな予感があります。
 作り直すときに比べてみるのが楽しみになりました(^^*)

 次回は、「落ち続ける」ゲームです。
 どうぞお楽しみに(*^v^)ノシ

 次の実験(落下ゲーム):「フォール・フィット」

コメントを残す