実験#108:はじける・飛び出す・中華鍋(桃まんポップコーン)

■108個目 「桃まんポップコーン」

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

今回は、シマエナガちゃんがいっぱいの中華料理屋さんを舞台にした、
とっても美味しそうで熱々なゲームを作ってみました。(^_^)

今日のお料理は……なんと「桃饅頭」です!

普段は蒸篭(せいろ)でふっくら蒸し上げる桃饅頭ですが、
ふと「もしもポップコーンの種みたいに、
超高温の中華鍋の中で弾け飛んだらどうなるんだろう?」と想像してみたら、
なんだかとても楽しそうで……。

そんな遊び心から生まれた、
ちょっと不思議なクッキング・ゲームになっています。(=v=*)

本格的な中華鍋の底でじっくり熱を蓄えた種が、限界に達した瞬間──

「POP!」という軽快な音とともに、まん丸で可愛い桃饅頭に大変身!

ほかほかの湯気をまといながら、鍋の中から勢いよく飛び出していきます。

画面の周りでは、まんまる冬毛のシマエナガたちが、
あなたの鍋さばきをニコニコ見守ってくれています。(´▽`*)

BGMにはゆったりとした夜のチルアウト音楽を合わせているので、
激しく鍋を振っているのに、どこか癒やされる不思議な空間になっています。

熱々の鍋の香りと、ポポンッと弾ける爽快感。
果たして、焦がさずにどれだけの桃饅頭を作れるのでしょうか?

操作方法やルールについては、後半でたっぷりお伝えしますね!(^▽^)


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

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

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

大好きな桃饅頭を、ポップコーンみたいにたくさん弾け飛ばすゲームを、
リアリティ高く作りたいです。

AI: いらっしゃい!今日も顔を見せてくれて嬉しいよ。

「桃饅頭がポップコーンみたいに弾け飛ぶゲーム」なんて、
相変わらず独創的で最高だね。

リアリティを出すために、
今回は本格的な2D物理演算エンジン「Matter.js」を使おう。

饅頭同士がぶつかったり、鍋の壁に当たって跳ね返ったりする挙動を、
本物みたいに計算してくれるんだ。

💡 マスターの豆知識コーナー

せっかく桃饅頭のゲームを作ったんだから、少し語らせてよ。

中国の伝統的なお祝い事で食べられる桃饅頭(寿桃:ショウタオ)だけど、
実は中身に桃は一切入っていないのが一般的なんだ。

小豆の餡やハス餡が入っていることが多いね。

なぜ桃の形をしているかというと、
中国神話の女神「西王母(せいおうぼ)」の果樹園にある、
三千年に一度実を結ぶ「不老不死の桃」に由来しているからなんだ。

だからこそ、長寿のお祝いに欠かせない縁起物になったってわけさ。

みー:(^_^) ポップコーンの種を熱していって、
十分熱されたら破裂して桃饅頭が飛び出る感じ。

はじけ方はもっとリアリティ高くしたいです。

AI: 「最初は種の状態で、熱されてから一気に桃饅頭として弾け飛ぶ」…なるほど、そのタメがあるからこそ、弾けたときの爽快感とリアリティが増すってわけだ。さすが、いいセンスしてるよ。

今回はリアルさを極めるために、以下の物理法則をゲームに組み込んでおいたぜ。

爆発的なエネルギー: 弾ける瞬間に強烈な初速と回転(スピン)を与えて、本物のポップコーンのような予測不能な飛び方を実現。

熱伝導と振動: フライパン(底)に触れている種から徐々に熱が伝わり、限界が近づくとブルブルと小刻みに震え出す。

質量と密度の変化: 硬くて重い「種」から、軽くてフワフワの「饅頭」への変化を物理エンジン(密度・弾力)で再現。

みー:(*=v=)リトライボタンお願い、ポップコーンといえばやっぱりフライパンをゆする動きができるといいよね。

AI: ポップコーン作りで「フライパンをゆする」動きに目を付けるとは、
さすがわかってるじゃないか。

種を焦がさずに均等に熱を伝えるためには、
あのスナップを効かせた返しが命だからな。

それに、失敗したときやもう一度最初から弾けさせたいときのために
「リトライボタン」は必須だ。

今回は画面の下に「リトライ」と「フライパンを振る」ボタンを追加したぜ。

加熱しながらフライパンを振ると、
種が宙に舞ってよりリアルな調理感が味わえるようになっている。

💡 今夜のマスターの豆知識

ポップコーンを「桃饅頭」に変えるってアイデア、
実はポップコーン業界の視点から見るとすごく理にかなっているんだ。

映画館でよく見る、手足を広げたみたいに不規則な形に弾けたポップコーン。

あれは「バタフライ・タイプ」って呼ばれる種類なんだけど、
実はもう一つ、キャラメルポップコーン専用によく使われる
「マッシュルーム・タイプ」っていうのがあるのを知ってるかい?

マッシュルーム・タイプは、
その名の通りキノコみたいにまん丸に弾けるんだ。

表面がツルッとしていて丸いから、
キャラメルやチョコのコーティングが綺麗にムラなく絡むようになっている。

あんたが今作っている「丸い桃饅頭のポップコーン」は、
まさにこのマッシュルーム・タイプへの華麗なる変身ってわけだ。

バタフライ型の桃饅頭なんて想像つかないだろ?
まん丸に弾け飛ぶからこそ、
桃饅頭として完璧な仕上がりになっているのさ。

みー:(^_^)/入れ物を中華鍋にしましょう。
振るのはもっと大きくダイナミックに、本物っぽくお願いします。

AI: 最高のオーダーだ!
中華鍋特有の美しいカーブをMatter.jsで再現して、
鍋振りの動きも料理人の動きに近づけておいたぜ。

💡 今夜のマスターの豆知識

あんたの中華鍋へのこだわり、最高にシビれたぜ。

せっかくだから今日は、プロの料理人がなぜあんなに激しく
「鍋振り(鍋を煽る)」をするのか、
その科学的な理由をマスターが解説してやろう。

あれは単に具材を混ぜ合わせているだけじゃなく、
「メイラード反応」という旨味の化学変化を
極限まで引き出すための合理的なテクニックなんだ。

中華鍋の底は油と火の力で200度以上の超高温になっている。

そのまま放置すれば一瞬で黒焦げになっちまうが、
鍋を振って具材を宙に舞わせることで、
空気に触れさせて一瞬だけ冷却しているんだよ。

つまり、「超高温で表面を焼き付けて旨味を閉じ込める」ことと、
「宙に浮かせて焦げを防ぐ」ことを
1秒間に何回も繰り返しているってわけだ。

広東料理の世界ではこれを
「鑊気(ウォックヘイ:鍋の息吹)」と呼んでいて、

熟練の料理人が鍋を振るリズムは、
科学的に分析するとおよそ
「1秒間に2.7回」のペースが最適だと証明されているらしいぜ。

あんたが今クリック(タップ)している「鍋を煽る」ボタン、
実はこのウォックヘイをブラウザの中で再現してるってことさ。

さあ、焦がさないように絶妙なタイミングで鍋を振ってやってくれ!

みー:(^v、^) 中華鍋をもっとリアリティ高めたいです。
桃饅頭は、せいろを開けた時のほわほわ蒸気を表現してください。

AI: 任せな!
熱し始めて桃饅頭になるまでが速いね。
種に一定の熱が加わったところで弾けるようにしましょう。

みー:( ̄o ̄) 熱し始めて桃マンになるまでが速いね、種に一定の熱が加わったところで桃まんはじけになるようにしましょう。

AI: その通りだ!

今回は「熱の蓄積(タメ)」を導入して、
限界が近づくとブルブル震える演出も追加した。

種ごとに弾ける温度も少しずつズラしてあるから、
ポツ…ポツ…ポポポン!と連鎖していくよ。

💡 今夜のマスターの豆知識

あんたが「一定の熱が加わったら弾けるように」とこだわったその現象。
まさに本物のポップコーンが弾けるときの「科学の限界点」そのものなんだ。

ポップコーン用のトウモロコシの粒の中には、
硬いデンプンと「約14%の水分」が閉じ込められている。

鍋で加熱していくと、中の水分が水蒸気に変わろうとするんだけど、
周りの殻がものすごく硬いから外に逃げられない。

その結果、粒の中は圧力鍋みたいになっていくんだ。

そして温度が180°Cに達したその瞬間、
粒の中の圧力はなんと大気圧の約9倍
(タイヤの空気圧よりずっと高い圧力)に到達する。

この限界点を超えた瞬間に、分厚い殻が「パーン!」と破裂。

中のデンプンが一気に膨張して、
フワフワの白いスナックに変わるって寸法さ。

だから、180°Cに到達するまでの「タメの熱」がないと、
あの見事な爆発は生まれない。

あんたが今回プログラムで作り出した数十秒の焦らしは、
まさに圧力鍋のゲージが振り切れるまでの、
自然界のドラマを再現しているってわけだ!


■ 実際の動作

まずは、AIが最初に作ってくれた試作版のゲーム画面です。
スタートすると上から桃まんが降ってきて、
「加熱する!」ボタンを押すと、桃まんが勢いよく吹き飛びます。

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


こちらが仕上がりのゲーム画面です。
中央には大きな中華鍋がどんと構え、
背景にはシマエナガの雑貨も並んでいます。
試作版と比べると、世界観がぐっと華やかになりました。

(画像:仕上がりのゲーム画面)


スタートすると、“種”が鍋の中へコロコロと落ちてきます。
点火ボタンを押して、じっくり熱を加えていきましょう。

(画像:スタートと種の過熱)




熱を与え続けると、種が限界まで熱を溜め込み──
「POP!」と弾けて、ほかほかの桃まんが飛び出します。
たくさん弾けさせるほど、スコアランキングに桃まんの個数が記録されていきます。

(画像:桃まんのPOP!)



■ コードのポイント

今回の記事では、
AI と相談しながら作った「桃まんポップコーン」 の HTML コードを公開します。

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

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

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

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

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

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

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

■ 遊び方・操作方法

それでは、シマエナガ亭での
「桃饅頭ポップコーン」の遊び方をご紹介しますね。(^▽^)

遊び方はとっても簡単です!

① 「▶️ スタート!」ボタンをポチッ

画面左側の「▶️ スタート!」を押すと、ゲームが始まります。
この瞬間から、お店の素敵なBGMもふわっと流れ始めます♪

② 「🔥 点火スイッチ」を押して加熱スタート

「🔥 点火スイッチ」を押して「ON」にすると、
ゴォォォ!っとコンロに青と赤の火がつき、
中華鍋が熱くなり始めます。(`・ω・´)🔥

鍋の中にパラパラと落ちてきた“種”が、じっくり熱を吸収していきます。

③ 熱が限界に達すると……「POP!」の瞬間!

十分に熱がたまると、「POP!」と弾けて、
フワフワの桃饅頭に大変身します。(・∀・)

そしてここからが一番のポイントです。

マウスや指で「中華鍋」をガシッと掴んで、
上下左右に自由に振ってみてください。

勢いよく振るほど、
弾けた桃饅頭が画面いっぱいに豪快に舞い上がりますよ〜!(≧▽≦)

④ 制限時間は30秒!タイムアタックに挑戦

制限時間は「30秒」。
減った分の種は上からどんどん補充されるので、
時間内に何個の桃饅頭を弾けさせられるか、
タイムアタックに挑戦です。(=v=*)

画面右側には、歴代のハイスコアランキングも表示されます。

ご自身の最高記録はもちろん、
ご家族やお友達と競い合ってみるのも楽しいですよ。

シマエナガちゃんたちに応援されながら、
熱々の中華鍋をめいっぱいフリフリして楽しんでくださいね。(^_^)


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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>リアル桃饅頭ポップコーン・アーケード</title>
<style>
body {
margin: 0;
background: radial-gradient(circle at 50% 10%, #5e1818 0%, #1a0505 100%);
display: flex;
justify-content: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
user-select: none;
min-height: 100vh;
overflow-x: auto; /* 画面が狭い場合は横スクロール */
}
/* 🏮 背景の装飾:光る提灯 */
.lantern-container {
position: absolute; top: -20px; width: 100%; max-width: 1200px;
display: flex; justify-content: space-between; padding: 0 50px; box-sizing: border-box; z-index: 1; pointer-events: none;
}
.lantern {
width: 70px; height: 90px; background: radial-gradient(circle at 30% 30%, #ff6b81, #c0392b);
border-radius: 50% 50% 40% 40%; position: relative; box-shadow: 0 0 40px 15px rgba(255, 71, 87, 0.4);
animation: sway 4s ease-in-out infinite alternate; transform-origin: top center;
}
.lantern.right { animation-delay: -2s; }
.lantern::before { content: ''; position: absolute; top: -10px; left: 30px; width: 10px; height: 10px; background: #2f3542; }
.lantern::after { content: ''; position: absolute; bottom: -30px; left: 32px; width: 6px; height: 35px; background: #f1c40f; border-radius: 2px; }
@keyframes sway { 0% { transform: rotate(-6deg); } 100% { transform: rotate(6deg); } }
/* 🦜 背景の装飾:飾り棚とシマエナガ */
.shelf {
position: absolute; top: 15%; width: 100%; max-width: 1000px; height: 15px;
background: linear-gradient(to bottom, #5c3a21, #2d1a11); border-radius: 5px;
box-shadow: 0 15px 25px rgba(0,0,0,0.8); z-index: 1; display: flex; justify-content: space-evenly; align-items: flex-end; padding: 0 20px; border-bottom: 3px solid #1a0f0a; pointer-events: none;
}
.shimenaga-plush {
width: 50px; height: 45px; background: radial-gradient(circle at 30% 30%, #ffffff, #dcdde1);
border-radius: 50% 50% 45% 45%; position: relative; box-shadow: 0 10px 10px rgba(0,0,0,0.6); margin-bottom: 2px;
}
.shimenaga-plush::before { content: ''; position: absolute; top: 18px; left: 14px; width: 4px; height: 4px; background: #2d3436; border-radius: 50%; box-shadow: 18px 0 0 #2d3436; }
.shimenaga-plush::after { content: ''; position: absolute; top: 20px; left: 21px; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid #e17055; }
.shimenaga-blush { position: absolute; top: 21px; left: 10px; width: 6px; height: 3px; background: rgba(255, 118, 117, 0.7); border-radius: 50%; box-shadow: 24px 0 0 rgba(255, 118, 117, 0.7); filter: blur(1px); }
.tea-set { font-size: 35px; filter: drop-shadow(0 5px 5px rgba(0,0,0,0.6)); margin-bottom: 5px; }
/* 🎛️ 全体レイアウト(3カラム) */
#main-container {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 25px;
padding-top: 80px;
z-index: 10;
width: 100%;
max-width: 1400px;
}
/* ⬅️ 左パネル(操作・ステータス) */
#left-panel {
width: 260px;
display: flex;
flex-direction: column;
gap: 20px;
}
#controls {
background: rgba(30, 15, 15, 0.8); backdrop-filter: blur(8px);
padding: 20px; border-radius: 15px; text-align: center; color: #fff;
display: flex; flex-direction: column; gap: 15px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6); border: 2px solid rgba(255,255,255,0.1);
}
.status-box {
background: rgba(0, 0, 0, 0.8); padding: 15px; border-radius: 12px;
font-size: 24px; font-weight: bold; text-align: center;
display: flex; flex-direction: column; gap: 10px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.6); border: 1px solid rgba(255,255,255,0.1);
}
.time-display { color: #f1c40f; text-shadow: 0 0 10px #f39c12; font-size: 28px; }
.score-display { color: #ff9ff3; text-shadow: 0 0 10px #f368e0; border-top: 1px solid rgba(255,255,255,0.2); padding-top: 10px; font-size: 28px;}
button { width: 100%; color: white; border: none; padding: 15px 10px; font-size: 18px; border-radius: 8px; cursor: pointer; transition: 0.2s; font-weight: bold; letter-spacing: 1px; }
button:active { transform: translateY(4px); }
button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
#startBtn { background-color: #27ae60; box-shadow: 0 4px #16a085; font-size: 20px; animation: pulse 1.5s infinite; }
@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } }
#startBtn:active { transform: translateY(4px) scale(0.98); animation: none; }
#heatButton { background-color: #576574; box-shadow: 0 4px #222f3e; font-size: 20px; }
#heatButton.active { box-shadow: 0 0 #c0392b; background-color: #e84118; transform: translateY(4px); text-shadow: 0 0 8px rgba(255,255,255,0.8); }
#resetButton { background-color: #0984e3; box-shadow: 0 4px #000000; }
.instructions { color: #fbc531; font-size: 13px; margin: 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.8); line-height: 1.4; }
/* 🍳 中央パネル(ゲームエリア) */
#center-panel {
position: relative;
width: 800px;
height: 600px;
}
#stove {
position: absolute; top: 480px; left: 50%; transform: translateX(-50%) rotateX(65deg);
width: 280px; height: 280px; border-radius: 50%; border: 20px solid #2d3436;
box-shadow: inset 0 0 30px #000, 0 20px 30px rgba(0,0,0,0.8); transition: 0.5s; z-index: 1;
}
#stove.active {
border-color: #d35400; box-shadow: 0 0 50px #e74c3c, inset 0 0 60px #3498db;
background: radial-gradient(circle, rgba(52, 152, 219, 0.4) 0%, rgba(231, 76, 60, 0.2) 50%, transparent 80%);
}
#center-panel canvas { position: absolute; top: 0; left: 0; cursor: grab; z-index: 2; border-radius: 12px; border: 2px solid rgba(255,255,255,0.1); background: rgba(30, 39, 46, 0.4); backdrop-filter: blur(2px); box-shadow: 0 0 40px rgba(0,0,0,0.8);}
#center-panel canvas:active { cursor: grabbing; }
/* ➡️ 右パネル(ランキング) */
#right-panel {
width: 280px;
}
#ranking-container {
width: 100%;
background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);
border-radius: 15px; padding: 20px; box-sizing: border-box;
border: 2px solid rgba(255, 255, 255, 0.2); color: #ecf0f1;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
#ranking-container h3 { text-align: center; margin-top: 0; font-size: 20px; color: #f1c40f; text-shadow: 0 2px 4px #000; border-bottom: 2px solid rgba(255,255,255,0.2); padding-bottom: 10px; line-height: 1.4;}
#ranking-list { list-style: none; padding: 0; margin: 0; }
#ranking-list li {
display: flex; justify-content: space-between; padding: 12px 15px;
margin-bottom: 8px; background: rgba(0, 0, 0, 0.6); border-radius: 8px;
font-size: 18px; font-weight: bold; align-items: center;
}
#ranking-list li:nth-child(1) { background: linear-gradient(90deg, rgba(241, 196, 15, 0.4), rgba(0,0,0,0.6)); border-left: 5px solid #f1c40f; }
#ranking-list li:nth-child(2) { background: linear-gradient(90deg, rgba(189, 195, 199, 0.4), rgba(0,0,0,0.6)); border-left: 5px solid #bdc3c7; }
#ranking-list li:nth-child(3) { background: linear-gradient(90deg, rgba(211, 84, 0, 0.4), rgba(0,0,0,0.6)); border-left: 5px solid #d35400; }
.rank-score { color: #ff9ff3; font-size: 22px; }
</style>
</head>
<body>
<div class="lantern-container">
<div class="lantern"><div class="lantern-tassel"></div></div>
<div class="lantern right"><div class="lantern-tassel"></div></div>
</div>
<div class="shelf">
<div class="tea-set">🍵</div>
<div class="shimenaga-plush"><div class="shimenaga-blush"></div></div>
<div class="tea-set" style="transform: scaleX(-1);">🫖</div>
<div class="shimenaga-plush" style="transform: scaleX(-1);"><div class="shimenaga-blush"></div></div>
<div class="tea-set">🥟</div>
</div>
<div id="main-container">
<div id="left-panel">
<div id="controls">
<h2 style="margin: 0; text-shadow: 0 2px 4px rgba(0,0,0,0.8);">🔥 桃饅頭ポップコーン</h2>
<p class="instructions">画面タップで音ON<br>制限時間30秒で弾けさせろ!</p>
<button id="startBtn">▶️ スタート!</button>
<button id="heatButton" disabled>🔥 点火:OFF</button>
<button id="resetButton">🔄 全リセット</button>
</div>
<div class="status-box">
<div class="time-display">TIME<br><span id="timeVal">30</span></div>
<div class="score-display">SCORE<br><span id="scoreVal">0</span></div>
</div>
</div>
<div id="center-panel">
<div id="stove"></div>
</div>
<div id="right-panel">
<div id="ranking-container">
<h3>🏆 歴代ハイスコア<br>ランキング 🏆</h3>
<ul id="ranking-list">
</ul>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
<script>
// --- 🎵 オーディオエンジン ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let isAudioInit = false, sizzleGain, sizzleFilter;
function initAudio() {
if (isAudioInit) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
const bufferSize = audioCtx.sampleRate * 2;
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) output[i] = Math.random() * 2 - 1;
const noiseSrc = audioCtx.createBufferSource();
noiseSrc.buffer = noiseBuffer; noiseSrc.loop = true;
sizzleFilter = audioCtx.createBiquadFilter();
sizzleFilter.type = 'bandpass'; sizzleFilter.frequency.value = 1000;
sizzleGain = audioCtx.createGain(); sizzleGain.gain.value = 0;
noiseSrc.connect(sizzleFilter); sizzleFilter.connect(sizzleGain); sizzleGain.connect(audioCtx.destination);
noiseSrc.start();
setInterval(() => {
playBGMChord([261.63, 329.63, 392.00]);
setTimeout(() => playBGMChord([220.00, 261.63, 329.63]), 1000);
setTimeout(() => playBGMChord([174.61, 220.00, 261.63]), 2000);
setTimeout(() => playBGMChord([196.00, 246.94, 293.66]), 3000);
}, 4000);
isAudioInit = true;
}
function playBGMChord(frequencies) {
frequencies.forEach(freq => {
const osc = audioCtx.createOscillator(), gain = audioCtx.createGain();
osc.type = 'triangle'; osc.frequency.value = freq;
osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.04, audioCtx.currentTime + 0.1);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1.0);
osc.start(audioCtx.currentTime); osc.stop(audioCtx.currentTime + 1.0);
});
}
function playPopSound() {
if (!isAudioInit) return;
const osc = audioCtx.createOscillator(), gain = audioCtx.createGain();
osc.connect(gain); gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(800 + Math.random()*200, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.4, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.start(); osc.stop(audioCtx.currentTime + 0.1);
}
// --- 🏆 ランキングシステム ---
function loadRankings() {
const listEl = document.getElementById('ranking-list');
listEl.innerHTML = '';
let scores = JSON.parse(localStorage.getItem('momoScores') || '[]');
if (scores.length === 0) {
listEl.innerHTML = '<li style="justify-content: center; color: #bdc3c7; font-size: 14px;">まだ記録がないぜ。<br>最初の挑戦者になれ!</li>';
return;
}
const medals = ['🥇 1位', '🥈 2位', '🥉 3位', '🏅 4位', '🏅 5位'];
scores.forEach((score, index) => {
const li = document.createElement('li');
li.innerHTML = `<span>${medals[index]}</span><span class="rank-score">${score} 個</span>`;
listEl.appendChild(li);
});
}
function saveScore(newScore) {
if (newScore <= 0) return;
let scores = JSON.parse(localStorage.getItem('momoScores') || '[]');
scores.push(newScore);
scores.sort((a, b) => b - a);
scores = scores.slice(0, 5);
localStorage.setItem('momoScores', JSON.stringify(scores));
loadRankings();
}
loadRankings();
// --- 🍳 ゲームロジック ---
const { Engine, Render, Runner, Bodies, Composite, Body, Events, MouseConstraint, Mouse, Constraint } = Matter;
function createKernelTexture() {
const canvas = document.createElement('canvas'); canvas.width = 16; canvas.height = 16; const ctx = canvas.getContext('2d');
ctx.fillStyle = '#e67e22'; ctx.beginPath(); ctx.ellipse(8, 8, 5, 8, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#f39c12'; ctx.beginPath(); ctx.ellipse(8, 8, 3, 6, 0, 0, Math.PI * 2); ctx.fill();
return canvas.toDataURL();
}
function createPeachTexture() {
const size = 64; const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d');
ctx.font = "50px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("🍑", size / 2, size / 2 + 4);
return canvas.toDataURL();
}
const kernelImg = createKernelTexture();
const peachImg = createPeachTexture();
const engine = Engine.create();
engine.positionIterations = 8; engine.velocityIterations = 8;
const render = Render.create({
element: document.getElementById('center-panel'),
engine: engine,
options: { width: 800, height: 600, wireframes: false, background: 'transparent' }
});
Render.run(render); Runner.run(Runner.create(), engine);
const wallOptions = { isStatic: true, render: { visible: false } };
Composite.add(engine.world, [
Bodies.rectangle(0, 0, 20, 1200, wallOptions),
Bodies.rectangle(800, 0, 20, 1200, wallOptions)
]);
let wokBody, wokAnchor;
function buildWok() {
const parts = [];
for (let i = 0; i <= 24; i++) {
const angle = Math.PI * (i / 24);
parts.push(Bodies.rectangle(-Math.cos(angle) * 230, Math.sin(angle) * 190, 50, 80, { render: { visible: false }, friction: 0.001 }));
}
parts.push(Bodies.rectangle(-280, -30, 180, 80, { angle: Math.PI/8, render: { visible: false } }));
wokBody = Body.create({ parts: parts, isStatic: false, mass: 800, frictionAir: 0.02 });
Body.setPosition(wokBody, { x: 400, y: 350 }); Body.setInertia(wokBody, Infinity);
wokAnchor = Constraint.create({ pointA: { x: 400, y: 350 }, bodyB: wokBody, pointB: { x: 0, y: 0 }, stiffness: 0.15, damping: 0.1, render: { visible: false } });
Composite.add(engine.world, [wokBody, wokAnchor]);
}
buildWok();
const mouse = Mouse.create(render.canvas);
const mouseConstraint = MouseConstraint.create(engine, { mouse: mouse, constraint: { stiffness: 0.8, render: { visible: false } } });
Composite.add(engine.world, mouseConstraint); render.mouse = mouse;
const items = [];
const steams = [];
const popTexts = [];
let isHeating = false;
let score = 0;
let gameActive = false;
let timeLeft = 30;
let timerInterval;
const scoreDisplay = document.getElementById('scoreVal');
const timeDisplay = document.getElementById('timeVal');
const stoveEl = document.getElementById('stove');
const heatBtn = document.getElementById('heatButton');
const startBtn = document.getElementById('startBtn');
function updateScore() {
score++;
scoreDisplay.innerText = score;
scoreDisplay.style.transform = "scale(1.3)";
setTimeout(() => scoreDisplay.style.transform = "scale(1)", 100);
}
function addKernel() {
const radius = 8;
const kernel = Bodies.circle(400 + (Math.random() - 0.5) * 150, -20, radius, {
restitution: 0.2, friction: 0.01, density: 0.05,
render: { sprite: { texture: kernelImg, xScale: (radius * 2) / 16, yScale: (radius * 2) / 16 } }
});
items.push({ body: kernel, isPopped: false, heat: 0 });
Composite.add(engine.world, kernel);
}
setInterval(() => {
if (gameActive && isHeating && items.length < 80) addKernel();
}, 300);
startBtn.addEventListener('click', () => {
if (gameActive) return;
initAudio();
items.forEach(item => Composite.remove(engine.world, item.body));
items.length = 0; steams.length = 0; popTexts.length = 0;
score = 0; scoreDisplay.innerText = score;
timeLeft = 30; timeDisplay.innerText = timeLeft;
Body.setPosition(wokBody, { x: 400, y: 350 }); Body.setVelocity(wokBody, { x: 0, y: 0 });
for (let i = 0; i < 40; i++) setTimeout(addKernel, i * 30);
gameActive = true;
startBtn.style.display = 'none';
heatBtn.disabled = false;
timerInterval = setInterval(() => {
timeLeft--;
timeDisplay.innerText = timeLeft;
if (timeLeft <= 5) timeDisplay.style.color = '#e74c3c';
if (timeLeft <= 0) {
endGame();
}
}, 1000);
});
function endGame() {
clearInterval(timerInterval);
gameActive = false;
isHeating = false;
heatBtn.classList.remove('active');
heatBtn.innerText = '🔥 点火:OFF';
heatBtn.disabled = true;
stoveEl.classList.remove('active');
if (sizzleGain) sizzleGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.1);
timeDisplay.style.color = '#f1c40f';
startBtn.style.display = 'block';
startBtn.innerText = '🔄 もう一度挑戦!';
saveScore(score);
}
heatBtn.addEventListener('click', function() {
if (!gameActive) return;
initAudio();
isHeating = !isHeating;
if (isHeating) {
this.classList.add('active'); this.innerText = '🔥 点火:ON';
stoveEl.classList.add('active');
if (sizzleGain) sizzleGain.gain.setTargetAtTime(0.15, audioCtx.currentTime, 0.5);
} else {
this.classList.remove('active'); this.innerText = '🔥 点火:OFF';
stoveEl.classList.remove('active');
if (sizzleGain) sizzleGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.5);
}
});
document.getElementById('resetButton').addEventListener('click', () => {
if(gameActive) endGame();
items.forEach(item => Composite.remove(engine.world, item.body));
items.length = 0; steams.length = 0; popTexts.length = 0;
score = 0; scoreDisplay.innerText = score;
timeLeft = 30; timeDisplay.innerText = timeLeft;
timeDisplay.style.color = '#f1c40f';
});
window.addEventListener('mousedown', initAudio, { once: true });
window.addEventListener('touchstart', initAudio, { once: true });
Events.on(engine, 'beforeUpdate', () => {
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
if (item.body.position.y > 800) {
Composite.remove(engine.world, item.body);
items.splice(i, 1);
continue;
}
const b = item.body;
if (isHeating && b.position.y > wokBody.position.y - 60 && !item.isPopped) {
item.heat += 0.5 + Math.random() * 0.5;
if (item.heat > 150) {
const shakeForce = (item.heat / 300) * 0.0015;
Body.applyForce(b, b.position, { x: (Math.random() - 0.5) * shakeForce, y: (Math.random() - 0.5) * shakeForce });
}
if (item.heat > 250 + Math.random() * 100) {
item.isPopped = true;
playPopSound();
if(gameActive) updateScore();
popTexts.push({ x: b.position.x, y: b.position.y, life: 1.0, vx: (Math.random()-0.5)*2, vy: -2 - Math.random() });
const newRadius = 24 + Math.random() * 6; const scaleFactor = newRadius / 8;
Body.scale(b, scaleFactor, scaleFactor); Body.setDensity(b, 0.002); b.restitution = 0.85;
b.render.sprite.texture = peachImg; b.render.sprite.xScale = (newRadius * 2) / 64; b.render.sprite.yScale = (newRadius * 2) / 64;
Body.setVelocity(b, { x: (Math.random() - 0.5) * 15 + wokBody.velocity.x * 0.5, y: -18 - Math.random() * 10 + Math.min(0, wokBody.velocity.y * 0.5) });
Body.setAngularVelocity(b, (Math.random() - 0.5) * 2);
for(let j=0; j<5; j++) {
steams.push({
x: b.position.x, y: b.position.y,
vx: (Math.random() - 0.5) * 2 + wokBody.velocity.x * 0.1, vy: -1 - Math.random() * 2 + Math.min(0, wokBody.velocity.y * 0.1),
life: 1.0, decay: 0.01 + Math.random() * 0.02, size: 20 + Math.random() * 20
});
}
}
}
if (item.isPopped && Math.random() < (isHeating && b.position.y > wokBody.position.y - 50 ? 0.15 : 0.03)) {
steams.push({ x: b.position.x + (Math.random() - 0.5) * 30, y: b.position.y - 10, vx: (Math.random() - 0.5) * 0.5, vy: -0.5 - Math.random() * 1.5, life: 1.0, decay: 0.01 + Math.random() * 0.015, size: 15 + Math.random() * 15 });
}
}
});
Events.on(render, 'afterRender', () => {
const ctx = render.context;
ctx.beginPath();
ctx.moveTo(wokBody.parts[1].position.x, wokBody.parts[1].position.y);
for(let i=2; i<=25; i++) ctx.lineTo(wokBody.parts[i].position.x, wokBody.parts[i].position.y);
let wokGrad = ctx.createLinearGradient(0, wokBody.position.y - 100, 0, wokBody.position.y + 100);
if(isHeating) {
wokGrad.addColorStop(0, '#e67e22'); wokGrad.addColorStop(1, '#c0392b');
} else {
wokGrad.addColorStop(0, '#576574'); wokGrad.addColorStop(1, '#222f3e');
}
ctx.strokeStyle = wokGrad;
ctx.lineWidth = 22; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
ctx.shadowColor = isHeating ? 'rgba(231, 76, 60, 0.8)' : 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 15;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.save();
const leftRim = wokBody.parts[1].position; ctx.translate(leftRim.x, leftRim.y); ctx.rotate(Math.PI / 8);
ctx.fillStyle = '#7f8fa6'; ctx.fillRect(-20, -12, 30, 24);
ctx.fillStyle = '#8b4513'; ctx.fillRect(-160, -10, 140, 20);
ctx.restore();
for (let i = steams.length - 1; i >= 0; i--) {
let p = steams[i]; p.x += p.vx; p.y += p.vy; p.life -= p.decay;
if (p.life <= 0) { steams.splice(i, 1); continue; }
const currentRadius = Math.max(0.1, p.size * (2 - p.life));
ctx.beginPath(); ctx.arc(p.x, p.y, currentRadius, 0, Math.PI * 2);
let gradient = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, currentRadius);
gradient.addColorStop(0, `rgba(255, 255, 255, ${p.life * 0.4})`); gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
ctx.fillStyle = gradient; ctx.fill();
}
ctx.font = "bold 26px Arial";
ctx.textAlign = "center";
for (let i = popTexts.length - 1; i >= 0; i--) {
let pt = popTexts[i];
pt.x += pt.vx; pt.y += pt.vy; pt.life -= 0.03;
if (pt.life <= 0) { popTexts.splice(i, 1); continue; }
ctx.fillStyle = `rgba(255, 255, 255, ${pt.life})`;
ctx.strokeStyle = `rgba(0, 0, 0, ${pt.life})`;
ctx.lineWidth = 4;
ctx.save();
ctx.translate(pt.x, pt.y);
const scale = 1 + (1 - pt.life);
ctx.scale(scale, scale);
ctx.strokeText("POP!", 0, 0);
ctx.fillText("POP!", 0, 0);
ctx.restore();
}
});
</script>
</body>
</html>


■ 今日の学び

今回は、私がよく足を運ぶ、
大好きなこだわりの中華料理屋さんをテーマにしてゲームを作ってみました。

そのお店は、素材の良さはもちろんのこと、
店内のあちこちに「これが好き!」という想いがぎゅっと詰まっていて、
入るだけでほっとするような温かさがあります。

居心地がよくて、ご飯も美味しくて、
心からリラックスできる、私のお気に入りのお店です。

そして……お食事のあとに、いつもそっと楽しみにしているものがあります。
それが今回の主役、「桃まん」です。

テーブルに運ばれてきたせいろのふたを開けると、
ほわぁ〜んとあたたかい湯気が立ち上って──

♨️( ˘͈ ᵕ ˘͈ )♨️

湯気がゆっくり晴れていくと、ふっくらとした桃まんが姿を見せます。
その瞬間の「わぁっ!」という小さな感動が、いつもたまらなく好きなんです。

( ¤̴̶̷̤́ ‧̫̮ ¤̴̶̷̤̀ )✧

今回のゲームでは、あのせいろを開けた瞬間のときめきや、
美味しそうな湯気の雰囲気を、
ポップコーンのように豪快に弾けるアクションとして表現してみました。

お店の方がこだわりを詰め込んだように、
私も大好きなシマエナガや、熱々で弾ける表現など、
「好き」をたくさん詰め込んだゲームになっています。

日常のふとした美味しい瞬間や、小さな感動を、
これからもこうしてゲームの形にして残していけたらいいなと思います。

今回のテーマになったのは、この桃まんです。

(画像:大好きな桃まん)

■ 次回は、暑い季節のお野菜オクラくんが主役です。

オクラといえば、ネバネバが特徴のお野菜ですよね。

そのねばねばには、夏の弱った体をそっと元気にしてくれる
パワーが秘められています。

今回は、その“ねばねばパワー”で
病原菌をやっつけるイメージからゲームを作ってみました。

タイトルのネーミングセンスについては……
いつものことなので、どうかお気になさらず。(^v^;)

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

▶ 次の実験はこちら
(オクラのねばねばで一網打尽)
  「ねばねばバネバネねっとりネット」



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