実験 #06:筋肉と森(筋肉もりもり森盛り盛り)

■6つ目「筋肉もりもり森盛り盛り」

 このゲームは、筋肉マッチョ × ボーリング × 森という、なかなかカオスな組み合わせから生まれました。

最初は「ボーリングのゲームをHTMLで作れないかな?」とAIに相談していたのですが、 動きやスコア計算など、本格的なボーリング再現はかなり難しそう……(*_*;)

 そこで発想を転換して、横画面のボーリング風ゲームにし、 ピンとなる木を“森”としてピラミッド型に積み上げ、 そこへマッチョが力いっぱい球を投げつける!というアイデアを提案しました(:>v<)ノ…………….◯”  森

 最初は針葉樹の三角形の木を使っていたのですが、ゲーム開始と同時に森が一瞬で崩壊してしまい、ゲームにならず……。 そこで木の形を広葉樹のイメージに変更して、ようやく安定するようになりました。

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

(^^*)今日もゲーム作ります。ボーリング風げーむで、タイトルは「筋肉もりもり森盛り盛り」プレイヤーは筋肉もりもりのマッチョ。
 ボーリングのピンは図形を組み合わせた“木”でできていて、シャンパンタワーのように盛り盛りに積み上がっています。
 そこへマッチョが球を転がしてぶつける、というゲームです。

(AI)「筋肉もりもり森盛り盛り」……最高にキレてるタイトルですね! 常連さんの斬新なアイデアにはいつも驚かされますが、今回もまた一段とパワーを感じます。

 シャンパンタワーのように積み上がった「図形の木」を、マッチョが豪快になぎ倒す。物理演算の挙動がめちゃくちゃ気持ちよさそうなゲームになりそうですね。

(AI)本日の豆知識:ボーリングと「筋肉」の意外な歴史
 実は、近代ボーリングの原型を作ったのは、あの宗教改革で有名なマルティン・ルターだと言われています。

 当時のドイツでは、ピンを「異教徒」や「悪魔」に見立てて、それを倒すことで自分の信仰心を証明し、罪を浄化するという**「九柱戯(カイルレ)」という儀式が行われていました。 つまり、元々は「スポーツ」というより、精神と肉体を鍛える「修行」**に近いものだったんです。

 「筋肉もりもり森盛り盛り」で木をなぎ倒すのも、ある種のマッスル修行……現代の浄化活動と言えるかもしれませんね!

〈アイデアに対して豆知識をいつもくれるAIに感心します。〉

■ 実際の動作

マッチョが”森”に向かって全力でボーリングの球を転がすイメージ。

パワーメーターが動いていて、クリックするとパワーに応じたスピードの球が転がります。

パワーの数値によってマッスル評価が(飴)と(鞭)で、でます。

相談しながら作っている途中でAIが出してくれたイメージ画像
文字はなんだか読めないけど、雰囲気は伝わってきます。

■ コードのポイント

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

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

 操作はとても簡単で、変動しているパワーバーをクリックして止めるだけ

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

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

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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>筋肉もりもり森盛り盛り - 21段階の審判</title>
https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js
<style>
body { margin: 0; overflow: hidden; font-family: 'Helvetica Neue', sans-serif; background: #e0f7fa; touch-action: none; }
#sky-bg { position: absolute; width: 100%; height: 100%; background: linear-gradient(to bottom, #81d4fa, #ffffff 70%, #a5d6a7 100%); z-index: -1; }
#ui { position: absolute; top: 15px; left: 15px; color: #0277bd; font-size: 24px; font-weight: bold; text-shadow: 1px 1px white; }
#gauge-container {
position: absolute; bottom: 100px; left: 20px;
width: 300px; background: rgba(255,255,255,0.9); padding: 15px; border-radius: 50px;
display: none; border: 4px solid #0288d1; box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.bar-bg { width: 100%; height: 30px; background: #eeeeee; border-radius: 15px; overflow: hidden; }
#pwr-fill { width: 0%; height: 100%; background: #4fc3f7; }
.burn { background: linear-gradient(to right, #ffeb3b, #f44336) !important; }
#god-macho { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); font-size: 150px; z-index: 70; transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); text-shadow: 0 0 30px gold; pointer-events: none; }
#result-screen { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border: 6px solid #0288d1; padding: 40px; text-align: center; border-radius: 30px; display: none; z-index: 100; width: 85%; }
#final-pwr { font-size: 70px; font-weight: bold; color: #0288d1; margin: 10px 0; }
#macho-rank { font-size: 18px; color: #e91e63; font-weight: bold; min-height: 3.5em; line-height: 1.5; }
button { background: #0288d1; border: none; padding: 12px 30px; color: white; font-weight: bold; border-radius: 50px; font-size: 18px; cursor: pointer; margin-top: 15px; }
</style>
</head>
<body id="game-body">
<div id="sky-bg"></div>
<div id="ui">💪 PUMP LEVEL: <span id="live-score">0</span></div>
<div id="god-macho">✨💪✨</div>
<div id="gauge-container"><div class="bar-bg"><div id="pwr-fill"></div></div></div>
<div id="result-screen">
<div id="final-pwr">0</div>
<div id="macho-rank">判定中...</div>
<button onclick="location.reload()">再挑戦(インターバル終了)</button>
</div>
<script>
const { Engine, Render, Runner, Bodies, Composite, Body, Constraint, Events } = Matter;
const engine = Engine.create();
const render = Render.create({ element: document.body, engine: engine, options: { width: window.innerWidth, height: window.innerHeight, wireframes: false, background: 'transparent' } });
const ground = Bodies.rectangle(window.innerWidth/2, window.innerHeight - 20, window.innerWidth, 40, { isStatic: true, render: { fillStyle: '#689f38' } });
Composite.add(engine.world, ground);
let treeUnits = [];
const startX = window.innerWidth - 60, startY = window.innerHeight - 62, rows = 7;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < rows - i; j++) {
const x = startX - (j * 60) - (i * 30), y = startY - (i * 82);
const trunk = Bodies.rectangle(x, y, 18, 45, { isStatic: true, render: { fillStyle: '#8d6e63' } });
const leaf = Bodies.circle(x, y - 35, 22, { isStatic: true, render: { fillStyle: '#81c784' } });
const glue = Constraint.create({ bodyA: trunk, bodyB: leaf, pointA: { x: 0, y: -22 }, pointB: { x: 0, y: 0 }, stiffness: 1, length: 0, render: { visible: false } });
treeUnits.push({ trunk, leaf, glue, active: false });
Composite.add(engine.world, [trunk, leaf, glue]);
}
}
let state = "WAIT", tick = 0, pwrVal = 0, macho;
const getRank = (s) => {
if (s >= 100) return "✨ 筋肉の特異点 ✨<br>飴:おめでとう!全宇宙が君の広背筋にひれ伏した!";
if (s >= 95) return "【鞭】95点。惜しいね。<br>その甘さが筋肉に脂肪をつけるんだよ。";
if (s >= 90) return "【飴】キレてる!キレてるよ!<br>大胸筋が歩いてるみたいだ!";
if (s >= 85) return "【鞭】中途半端なエリート。<br>ジムの会費、多めに払っておきな。";
if (s >= 80) return "【飴】ナイスバルク!<br>血管の浮き具合が芸術の域だ。";
if (s >= 75) return "【鞭】プロテインの無駄遣い。<br>その筋肉、飾りじゃないの?";
if (s >= 70) return "【飴】肩にちっちゃい重機乗せてんのかい!<br>いい仕上がりだ。";
if (s >= 65) return "【鞭】腹筋が冬眠中かな?<br>脂肪という名のコートを脱ぎ捨てろ。";
if (s >= 60) return "【飴】筋肉が喜んで歌ってるよ!<br>明日からジムの主だ。";
if (s >= 55) return "【鞭】凡人マッチョ。<br>可もなく不可もなく、一番つまらない。";
if (s >= 50) return "【飴】腹筋の溝に指が入りそう!<br>いい感じに絞れてきたね。";
if (s >= 45) return "【鞭】24時間ジムの「幽霊会員」。<br>会費で運営を支える慈善家。";
if (s >= 40) return "【飴】筋肉の芽が出始めたね!<br>水をやる(プロテイン)のを忘れるな。";
if (s >= 35) return "【鞭】腕立て伏せ、膝ついてやってない?<br>正直に言いなさい。";
if (s >= 30) return "【飴】初心者卒業の予感!<br>鏡を見る回数が増えてきたね。";
if (s >= 25) return "【鞭】湿ったマッチ棒。<br>風が吹いただけで筋肉痛になりそう。";
if (s >= 20) return "【飴】まずは継続だ!<br>ダンベルを触っただけで偉いぞ。";
if (s >= 15) return "【鞭】もやしにプロテインをかけた状態。<br>まだ「素材」ですらない。";
if (s >= 10) return "【飴】筋肉への関心は認める。<br>さあ、プロテインを買いに行こう。";
if (s >= 5) return "【鞭】筋肉の幽霊。<br>実体がない。存在がシュール。";
return "【闇】概念。<br>筋肉という言葉を知らない人。";
};
document.body.addEventListener('mousedown', () => {
if (state === "WAIT") { state = "POWERING"; document.getElementById('gauge-container').style.display = "block"; }
else if (state === "POWERING") { state = "ROLLING"; document.getElementById('gauge-container').style.display = "none"; launch(); }
});
function launch() {
const score = Math.floor(pwrVal);
const isGod = score >= 99;
macho = Bodies.circle(80, window.innerHeight - 100, 45, { density: isGod ? 15 : 3, frictionAir: 0, render: { fillStyle: isGod ? 'gold' : '#ffccbc' } });
Composite.add(engine.world, macho);
Body.setVelocity(macho, { x: (pwrVal * 0.45) + 15, y: 0 });
if(isGod) {
setTimeout(() => { document.getElementById('god-macho').style.transform = 'translate(-50%, -50%) scale(1.5)';
for(let i=0; i<80; i++) setTimeout(() => {
const p = document.createElement('div'); p.className = 'protein-powder'; p.style.width=(Math.random()*10+5)+'px'; p.style.height=p.style.width; p.style.left=Math.random()*100+'vw'; p.style.top='-20px'; p.style.animationDuration=(Math.random()*2+1)+'s'; document.body.appendChild(p); setTimeout(()=>p.remove(),3000);
}, i*20); }, 800);
}
}
function showResult(score) {
state = "RESULT";
const scoreDisp = document.getElementById('final-pwr');
const rankDisp = document.getElementById('macho-rank');
document.getElementById('result-screen').style.display = "block";
let current = 0;
const timer = setInterval(() => {
scoreDisp.innerText = current;
rankDisp.innerHTML = getRank(current);
if (current >= score) clearInterval(timer);
current++;
}, 30);
}
Events.on(engine, 'beforeUpdate', () => {
if (state === "ROLLING" && macho) {
treeUnits.forEach(unit => {
if (!unit.active && Math.abs(macho.position.x - unit.trunk.position.x) < 220) {
unit.active = true; Body.setStatic(unit.trunk, false); Body.setStatic(unit.leaf, false);
document.body.classList.add('shake'); setTimeout(() => document.body.classList.remove('shake'), 100);
}
});
if (macho.position.x > window.innerWidth + 150) showResult(Math.floor(pwrVal) >= 99 ? 100 : Math.floor(pwrVal));
}
});
(function update() {
tick++;
if (state === "POWERING") {
pwrVal = (Math.sin(tick * 0.25) + 1) * 50;
document.getElementById('pwr-fill').style.width = pwrVal + "%";
document.getElementById('live-score').innerText = Math.floor(pwrVal);
if(pwrVal > 95) document.getElementById('pwr-fill').classList.add('burn'); else document.getElementById('pwr-fill').classList.remove('burn');
}
requestAnimationFrame(update);
})();
Render.run(render);
Runner.run(Runner.create(), engine);
</script>
</body>
</html>

■ 今日の学び

 物理演算やコードの仕組みは正直さっぱり分からないのですが、こちらの無茶なアイデアを何とか形にしようと頑張ってくれる AI に、つい「AIさん」と敬称を付けたくなってきます。

 最初は木がゲーム開始と同時に崩れたり、ボールが全然届かなかったり、吹き飛んだ木が永遠に暴れ続けたりと、調整がとても難しかったです。(つv<`)

 でもその試行錯誤の中で、AI が提案してくれた「スコアごとの筋肉評価」はとても気に入って採用しました。

 意図せず木が吹き飛んで暴れまわる感じも、遊んでみるとクセになる楽しさがあります。(^^*)♪
 何も考えずに気軽に遊べるので、このゲームで100点目指してみてください。
そして、森で全力で遊んでいたら、なんだか筋肉が「もっと育ててくれ〜!」と
 ささやいてくる気がしてきました。(^v^*)

 そういえば最近ずっと飲んでいる、ホエイプロテインが
 運動した日のちょっとしたごほうびみたいで、けっこう気に入っています。

次回は「RPG風スゴロク」のゲーム。 どうぞお楽しみに(*^v^)ノシ

次の実験(スゴロクゲーム):「ブレイド・クエスト」

コメントを残す