■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^)ノシ
次の実験(落下ゲーム):「フォール・フィット」

コメントを残す