実験#45:太古の世界、神々、UFO(High & Low: 原始の鼓動)

■45個目「High & Low: 原始の鼓動

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

今回は、少しスケールの大きな「ハイ&ロー」ゲームを作ってみました。 舞台はなんと……原始時代の巨大火山です!

画面の左右から「赤神さま」と「白神さま」が登場し、 火山の火口(カゴ)に向かってエネルギーの玉をドカドカ投げ込んでいきます。

今回こだわったのは、だんだんハイテンションになっていく“原始の儀式っぽさ”。 「連続で正解すると、太鼓のビートが加速して、空の色や時間まで変わっていったらどうなるんだろう?」 そんな遊び心から生まれた、ちょっとトランス状態になれるゲームです。

この玉入れには、こんなお楽しみ要素が詰まっています。

  • 赤と白の神々による、大迫力の玉入れ競争
  • 連続正解で、太鼓のリズムと玉のスピードがどんどん加速
  • スコアに応じて、夜 → 朝 → 昼 → 夕方へと空の色が変化
  • 気まぐれに飛び回るUFOが、視界を惑わすお邪魔キャラとして登場

ズンチャ、ズンチャという太鼓の重低音に合わせて火山が脈打ち、 最後は「ドッカン!」と大噴火。 火口に入った玉がポポポンッ!と空へ打ち上がります。

さらに、UFOの妨害をかいくぐって連続正解していくと…… なんと火山からマグマが流れ出す演出も!? 奇跡の「同点クリア」も用意しているので、 どんな風に画面がカオスになっていくか、お楽しみに!( ´ ▽ ` )ノ

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

■ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。
今回のゲームは、ハイアンドロウ × 原始時代の神々の玉入れ。

双子火山の大穴に、赤神と白神がエネルギーボールをどんどん放り込みます。
10秒間で投げ込んだ数を比べて、赤神の数が白神より上か下かを予想します。

選んだら火山がゴゴゴゴ…ドッカン!と爆発して、結果の数字が飛び出します。
連続正解数がスコアになります。火口に放り込まれる様子から数を予測します。

AI: 素晴らしいアイデアですね!
「ハイアンドロウ」と「原始時代の神々の玉入れ」を組み合わせるとは壮大です。

原始時代なら、きっと「赤い石(炎)」と「白い石(光)」がエネルギーボールの代わりだったかもしれませんね。
ぜひ原始の感覚を研ぎ澄ませて、どちらが多く入るか予測してみてください。

みー:(^_^)* クオリティを高めていきましょう。
もっと大きな画面で、下から火口に向かって大量に放り込む描写が欲しいです。
玉は小さくていいので、一度に無数の球が火口へ向かって飛んでいく感じにしたいです。

AI: 素晴らしい!「大量に放り込む描写」、大変燃えるお話です。
さっそく原始の神々の玉入れを、より壮大な大画面ハイアンドロウへ進化させました。

  • 描写の強化:火山の振動と爆発演出をパワーアップ。数字も巨大に飛び出します。
  • 大画面化:ゲームエリアを拡大し、視認性を向上。
  • 大量玉入れ:下から巨大な火口へ向けて、無数のエネルギーボールが放物線を描いて飛び交います。

みー:(つv<`) 玉が入ったかどうか分からないから、運ゲームになってしまってる。 入ったり外れたりが見えるようにして、感覚で「こっちが多いかな?」と予測できるバランスにしたいね。

AI: ……痛いところを突かれました。
確かに、ただボールが消えるだけでは「どちらが多く入ったか」が分からず、完全に運任せでした。
マスターの設計ミスです、お恥ずかしい!

そこで、 「入った球」と「外れて落ちた球」がはっきり分かるように軌道を調整し、描写を改善しました。
じっくり観察して、どちらが多く入ったか推理してみてください。

📖 今日のトリビア:日本の「玉入れ」の厳格すぎる公式ルール

ゲームの話から少し離れて、今日は玉入れそのものの豆知識をひとつ。

運動会でおなじみの玉入れですが、実は 「全日本玉入れ選手権(アジャタ)」 という公式競技が存在します。

公式ルールでは、カゴの高さが 4メートル12センチ と厳密に決められています。 バスケットゴールより高いこの数字、実は…

北海道・和寒町で記録された 「最低気温 −41.2℃」 に由来しているんです。

さらに、カゴの直径(44cm)や深さ(44cm)も、和寒町が 北緯44度 にあることから決められています。

今回の神々の玉入れはさすがに4mでは済みませんが、 「入った数を目で追う楽しさ」を味わっていただければ嬉しいです。

〈今回もAIがたくさんのトリビアを教えてくれて、より楽しくゲーム作りができました〉(^^

■ 実際の動作

今回AIが最初に出してくれたゲームの初期の画面です。
最初は双子山の設定にしていましたが、最終的には一つの火山にしています。
(画像:AIが出してくれた初期のゲーム)


今回の仕上がりの状態です。
(画像:火口に向かって投げ込む様子)


投げ終わったら、どちらが多いかを選びます。

(画像:選択場面)

選択すると、投げ込んだ数の玉が、「ドッカン!!」という噴火とともに吹き出します。

(画像:噴火の様子)

こちらが 花畑です。やさしい色合いに小鳥のさえずりと時折聞こえるミツバチの羽音が心地よいです。

(画像:花畑の画面)

クリアするごとに時間も進み、ペースアップしていきます。
不規則な動きのUFOが気になります。

(画像:ゲーム画面)

細かな修正をしていく中で、AIが画像を出してくれます。

(画像:ゲーム作成中にAIが出してくれた画像)


■ コードのポイント

今回の記事では、AI と相談しながら作った High & Low: 原始の鼓動 の HTML コードを公開します。

ブラウザだけで動く、とてもシンプルなゲームです。 コードも難しい部分はほとんどないので、気軽に試してみてくださいね。

【遊ぶまでの手順】

1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「tamaire.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。

🌋 遊び方・操作方法 🌋

この「原始の鼓動」、ド派手な見た目ですが、 遊び方はとってもカンタンです!(^_^) 直感で楽しめるように作りました。

① 「玉入れ開始!」ボタンをポチッ

(※太鼓の音や爆発音が鳴るので、音量には注意してくださいね!) ボタンを押すと、10秒間の玉入れタイムがスタートします。

② ひたすらカゴ(火口)に集中する!

赤神さまの「赤い玉(🔥)」と、 白神さまの「白い玉(❄️)」、 どっちが多く火口に入ったか、よーく見て予想してください。

途中、変な動きをするUFO🛸がジャマをしてきますが…… 視線を奪われないように注意です!(><)

③ 10秒後に「赤」か「白」を選ぶ

玉入れが終わると、画面の下に2つのボタンが現れます。 「赤(🔥)が多い」か「白(❄️)が多い」、自分の目を信じてクリック!

④ ドッカン!と結果発表

選んだ瞬間、画面が大きく揺れて火山が大噴火! 火口に入った実際の数がドバーッと吹き出して答え合わせができます。

見事正解すると、連続正解数がアップ! そのまま次の玉入れに進めますが…… 正解するたびに、太鼓のビートも玉のスピードもどんどん速くなっていきます。(`・ω・´)

ちなみに、赤と白がまったく同じ数になる「奇跡の同点」だった場合は、 どちらを選んでいても無条件で「大正解」になる嬉しいおまけ付きです。(=v=*)

何度も遊んでいると、だんだん太鼓のリズムに乗ってきて、 不思議と“トランス状態”を味わえるかもしれません(笑)。

何回連続で正解できるか、ぜひ原始の儀式に挑戦してみてくださいね!( ^v^)ノ

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>High & Low: 原始の鼓動</title>
<style>
body { font-family: 'Georgia', serif; background-color: #2c2115; color: #f5eeda; margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; transition: background 0.8s ease; }
.container { background-color: #3e2f20; padding: 1.5rem; border-radius: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.7); text-align: center; width: 900px; height: 750px; position: relative; border: 4px solid #5a402a; display: flex; flex-direction: column; }
h1 { font-size: 2.2rem; color: #e74c3c; text-shadow: 3px 3px 5px #000; margin: 0 0 10px 0; }
.info { display: flex; justify-content: space-between; margin-bottom: 10px; font-size: 1.3rem; }
.score { color: #f1c40f; font-weight: bold;}
.timer { color: #fff; font-weight: bold; }
/* ゲームエリア */
.game-area { position: relative; width: 100%; flex-grow: 1; background-color: #1a1510; border-radius: 5px; border: 2px solid #5a402a; overflow: hidden; margin-bottom: 15px; transition: background 0.8s ease; }
/* 背景色(空)の定義 */
.sky-night { background: linear-gradient(to top, #0c1445, #000 70%); }
.sky-early-morning { background: linear-gradient(to top, #16245c, #0d1445 70%); }
.sky-sunrise { background: linear-gradient(to top, #f39c12, #c0392b 40%, #16245c 80%); }
.sky-day { background: linear-gradient(to top, #87ceeb, #fdfcfb); }
.sky-evening { background: linear-gradient(to top, #ff7e5f, #feb47b); }
.sky-sunset { background: linear-gradient(to top, #2c3e50, #ff4e50 50%, #ff7e5f); }
/* 激震アニメーション */
@keyframes megaShake { 0%, 100% { transform: translate(0, 0) rotate(0); } 10%, 30%, 50%, 70%, 90% { transform: translate(-20px, -20px) rotate(-3deg); } 20%, 40%, 60%, 80% { transform: translate(20px, 20px) rotate(3deg); } }
.mega-shake { animation: megaShake 0.7s cubic-bezier(.36,.07,.19,.97) both; }
/* ビート連動の脈打ちアニメーション */
@keyframes beatShake { 0% { transform: translateX(-50%) scale(1); } 50% { transform: translateX(-50%) scale(1.03) translateY(-3px); filter: brightness(1.2); } 100% { transform: translateX(-50%) scale(1); filter: brightness(1); } }
.beat-shake { animation: beatShake 0.15s ease-out; }
/* 富士山シルエット */
.volcano-silhouette { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80%; height: 65%; background: linear-gradient(to top, #3e2f20, #4a3c2e); clip-path: polygon(15% 100%, 85% 100%, 60% 0, 40% 0); z-index: 1.05; border-bottom: 5px solid #5a402a; transform-origin: bottom center; transition: all 1s ease; }
/* 2周目以降のマグマ流れ */
@keyframes magmaFlow { 0% { background-position: 0 0; } 100% { background-position: 0 100px; } }
.volcano-silhouette.magma-active::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(180deg, transparent, transparent 50%, #c0392b, #e74c3c); background-size: 100% 100px; animation: magmaFlow 2s linear infinite; opacity: 0.7; z-index: 1.06; }
/* 火口 */
.crater-container { position: absolute; bottom: 65%; left: 50%; transform: translate(-50%, 50%); width: 22%; height: 55px; z-index: 2; }
.crater-back { position: absolute; width: 100%; height: 100%; background-color: #1a1510; border-radius: 50%; border: 3px solid #2c2115; box-shadow: inset 0 10px 30px rgba(0,0,0,0.9); }
.crater-glow { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 40%; height: 40%; background: radial-gradient(circle, #f39c12 0%, transparent 70%); border-radius: 50%; opacity: 0.5; z-index: 3; }
.crater-front { position: absolute; width: 100%; height: 100%; border-radius: 50%; border-bottom: 6px solid #5a402a; z-index: 6; pointer-events: none; }
/* 噴煙 */
@keyframes plumeRise { 0% { transform: translateY(0) scale(1); opacity: 0.2; } 50% { transform: translateY(-50px) scale(1.5); opacity: 0.1; } 100% { transform: translateY(-100px) scale(2); opacity: 0; } }
.plume { position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 30px; height: 100px; background: radial-gradient(circle, rgba(200, 200, 200, 0.4) 0%, transparent 70%); border-radius: 50% 50% 0 0; z-index: 5; animation: plumeRise 3s ease-out infinite; opacity: 0.5; }
.throwing .plume { opacity: 0.8; animation: plumeRise 1.5s ease-out infinite; }
/* 飾り */
.emoji-decoration { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1.1; opacity: 0.8; transition: all 1s ease; }
.emoji-decoration span { position: absolute; font-size: 2rem; filter: sepia(30%) saturate(70%); }
.sky-day ~ .emoji-decoration span, .sky-early-morning ~ .emoji-decoration span { filter: none; }
/* 神々 */
.god { position: absolute; bottom: 20px; font-size: 4rem; text-shadow: 2px 2px 5px #000; z-index: 10; }
#red-god { left: 15%; transform: translateX(-50%); color: #e74c3c; }
#white-god { left: 85%; transform: translateX(-50%); color: #fff; }
/* ボール(巨大化) */
.ball { position: absolute; width: 24px; height: 24px; border-radius: 50%; opacity: 0; margin-left: -12px; margin-top: -12px; transition: none; }
.ball.hit { z-index: 4; animation: throwHitCenter var(--speed) cubic-bezier(0.25, 0.8, 0.5, 1) forwards; }
@keyframes throwHitCenter { 0% { transform: translate(0, 0) scale(1); opacity: 1; } 50% { transform: translate(calc(var(--tx) / 2), -250px) scale(1.3); opacity: 1; } 90% { transform: translate(var(--tx), calc(var(--ty) - 15px)) scale(0.6); opacity: 1; } 100% { transform: translate(var(--tx), calc(var(--ty) + 10px)) scale(0); opacity: 0; } }
.ball.miss { z-index: 7; animation: throwMissCenter calc(var(--speed) + 0.2s) cubic-bezier(0.25, 0.8, 0.7, 1) forwards; }
@keyframes throwMissCenter { 0% { transform: translate(0, 0) scale(1); opacity: 1; } 40% { transform: translate(calc(var(--tx) / 2), -200px) scale(1.2); opacity: 1; } 100% { transform: translate(calc(var(--tx) + var(--mx)), calc(var(--ty) + 300px)) scale(0.6); opacity: 0; } }
.ball.red { background-color: #e74c3c; box-shadow: 0 0 15px rgba(231, 76, 60, 1); }
.ball.white { background-color: #fff; box-shadow: 0 0 15px rgba(255, 255, 255, 1); }
/* --- UFO 5つの凶悪パターン --- */
#ufo { position: absolute; font-size: 5rem; z-index: 15; pointer-events: none; opacity: 0; filter: drop-shadow(0 0 15px rgba(0, 255, 0, 0.7)); }
@keyframes ufoFly1 { 0% { transform: translate(100vw, -100px) rotate(0deg); opacity: 0; } 10% { opacity: 1; transform: translate(80vw, 50px) rotate(10deg); } 30% { transform: translate(20vw, 150px) rotate(-10deg); } 50% { transform: translate(50vw, 50px) rotate(0deg); opacity: 1; } 70% { transform: translate(20vw, 200px) rotate(10deg); opacity: 0; } 80% { transform: translate(80vw, 100px) rotate(-10deg); opacity: 1; } 100% { transform: translate(-100px, -100px) rotate(0deg); opacity: 0; } }
.throwing #ufo.pattern-1 { animation: ufoFly1 6s linear infinite; }
@keyframes ufoFly2 { 0%, 100% { transform: translate(-100px, 100px) scale(0.5); opacity: 0; } 10% { transform: translate(10vw, 80px) scale(1); opacity: 1; } 30% { transform: translate(40vw, 80px) scale(1); opacity: 1; } 35% { transform: translate(40vw, 150px) scale(1.2); opacity: 1; } 45% { transform: translate(80vw, 20px) scale(0.8); opacity: 0; } 60% { transform: translate(70vw, 180px) scale(1.5); opacity: 1; } 80% { transform: translate(20vw, 200px) scale(1); opacity: 1; } 90% { transform: translate(100vw, 0px) scale(0.5); opacity: 0; } }
.throwing #ufo.pattern-2 { animation: ufoFly2 5s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite; }
@keyframes ufoFly3 { 0%, 100% { transform: translate(50vw, -100px) scale(0.5); opacity: 0; } 20% { transform: translate(50vw, 50px) scale(1); opacity: 1; } 40% { transform: translate(45vw, 150px) scale(1.2) rotate(-5deg); opacity: 1; } 60% { transform: translate(55vw, 150px) scale(1.2) rotate(5deg); opacity: 1; filter: drop-shadow(0 0 30px rgba(0, 255, 0, 1)); } 80% { transform: translate(50vw, -50px) scale(0.8); opacity: 0.5; } }
.throwing #ufo.pattern-3 { animation: ufoFly3 6s ease-in-out infinite; }
@keyframes ufoFly4 { 0% { transform: translate(-10vw, 100px) scale(0.8); opacity: 0; } 10% { opacity: 1; } 25% { transform: translate(30vw, 0px) scale(1.2) rotate(15deg); } 50% { transform: translate(50vw, 150px) scale(0.9) rotate(-15deg); } 75% { transform: translate(70vw, 0px) scale(1.2) rotate(15deg); } 90% { opacity: 1; } 100% { transform: translate(110vw, 100px) scale(0.8); opacity: 0; } }
.throwing #ufo.pattern-4 { animation: ufoFly4 4s ease-in-out infinite alternate; }
@keyframes ufoFly5 { 0% { transform: translate(50vw, -100px) scale(0.5); opacity: 0; } 20% { transform: translate(50vw, 250px) scale(1.5) rotate(720deg); opacity: 1; } 30% { transform: translate(30vw, 150px) scale(1) rotate(0deg); } 40% { transform: translate(50vw, 250px) scale(1.2) rotate(-360deg); } 50% { transform: translate(70vw, 150px) scale(1) rotate(0deg); } 60% { transform: translate(50vw, 250px) scale(1.5) rotate(720deg); } 80% { transform: translate(50vw, -100px) scale(0.5) rotate(0deg); opacity: 0; } 100% { opacity: 0; } }
.throwing #ufo.pattern-5 { animation: ufoFly5 5.5s cubic-bezier(0.25, 1, 0.5, 1) infinite; }
/* --- 吹き出す実体の玉(入れた数だけ出る) --- */
.erupt-ball { position: absolute; width: 22px; height: 22px; border-radius: 50%; z-index: 26; pointer-events: none; animation: eruptFly 1.5s cubic-bezier(0.25, 1, 0.5, 1) forwards; margin-left: -11px; margin-top: -11px; }
.erupt-ball.red { background-color: #e74c3c; box-shadow: 0 0 15px rgba(231, 76, 60, 1); border: 2px solid #fff; }
.erupt-ball.white { background-color: #fff; box-shadow: 0 0 15px rgba(255, 255, 255, 1); border: 2px solid #e74c3c; }
@keyframes eruptFly {
0% { transform: translate(var(--start-x), var(--start-y)) scale(0.5); opacity: 0; }
10% { opacity: 1; }
/* 上空へアーチを描く */
50% { transform: translate(calc(var(--start-x) + (var(--end-x) - var(--start-x))*0.5), calc(var(--start-y) - 300px)) scale(1.5); opacity: 1; }
/* 下へ落ちて消える */
100% { transform: translate(var(--end-x), calc(var(--start-y) + 300px)) scale(1); opacity: 0; }
}
/* 爆発細かいパーティクル(演出用) */
.particle { position: absolute; width: 10px; height: 10px; border-radius: 50%; z-index: 25; pointer-events: none; animation: shootOut 0.8s ease-out forwards; margin-left: -5px; margin-top: -5px; }
@keyframes shootOut { 0% { transform: translate(var(--start-x), var(--start-y)) scale(1); opacity: 1; } 100% { transform: translate(var(--end-x), var(--end-y)) scale(0); opacity: 0; } }
/* フラッシュと結果表示 */
@keyframes flashBang { 0% { background-color: rgba(255, 255, 255, 1); } 30% { background-color: rgba(255, 255, 255, 0.8); } 100% { background-color: rgba(0, 0, 0, 0.85); } }
.result-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; justify-content: center; align-items: center; flex-direction: column; z-index: 30; border-radius: 5px; }
.result-overlay.active { display: flex; animation: flashBang 1s ease-out forwards; }
.explosion-text { font-size: 4rem; color: #f1c40f; text-shadow: 0 0 20px #e74c3c, 5px 5px 0 #000; font-weight: bold; margin-bottom: 25px; animation: explodeText 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.result-nums { display: flex; justify-content: center; gap: 80px; }
.result-num { font-size: 8rem; font-weight: bold; text-shadow: 0 0 30px rgba(255,255,255,0.5), 8px 8px 0 #000; animation: popNum 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.result-num.red { color: #e74c3c; }
.result-num.white { color: #fff; }
@keyframes explodeText { 0% { transform: scale(0) rotate(-10deg); opacity: 0; } 100% { transform: scale(1) rotate(0); opacity: 1; } }
@keyframes popNum { 0% { transform: scale(0) translateY(50px); opacity: 0; } 100% { transform: scale(1) translateY(0); opacity: 1; } }
/* コントロール */
.controls { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 15px; }
button { padding: 15px 25px; font-size: 1.3rem; cursor: pointer; border: none; border-radius: 5px; color: white; transition: 0.3s; text-shadow: 1px 1px 3px #000; }
#start-btn { grid-column: 1 / 3; background-color: #f1c40f; color: #3e2f20; font-weight: bold; }
#start-btn:hover { background-color: #f39c12; }
#start-btn:disabled { background-color: #7f8c8d; cursor: not-allowed; }
.guess-btn { display: none; font-weight: bold; }
#btn-high { background-color: #e74c3c; }
#btn-low { background-color: #7f8c8d; }
</style>
</head>
<body>
<div class="container">
<h1>High & Low: 原始の鼓動</h1>
<div class="info">
<span class="score">連続正解: <span id="score-num">0</span></span>
<span class="timer">残り: <span id="timer-num">--</span></span>
</div>
<div class="game-area sky-night" id="game-area">
<div id="ufo">🛸</div>
<div class="volcano-silhouette" id="volcano"></div>
<div class="crater-container" id="crater">
<div class="crater-back"></div>
<div class="crater-glow"></div>
<div class="crater-front"></div>
<div class="plume"></div>
</div>
<div class="emoji-decoration">
<span style="left: 10%; bottom: 10%;">🛖</span>
<span style="left: 20%; bottom: 15%;">🦣</span>
<span style="left: 30%; bottom: 5%;">🦴</span>
<span style="left: 70%; bottom: 5%;">🪨</span>
<span style="left: 80%; bottom: 15%;">👩</span>
<span style="left: 90%; bottom: 10%;">🧔</span>
<span style="left: 35%; bottom: 25%;">🪨</span>
<span style="left: 65%; bottom: 20%;">🦴</span>
<span style="left: 5%; top: 5%;">☀️</span>
<span style="right: 5%; top: 10%;">🌕</span>
</div>
<span class="god" id="red-god">🔥</span>
<span class="god" id="white-god">❄️</span>
<div class="result-overlay" id="result-overlay">
<div class="explosion-text" id="result-text">ドッカン!!</div>
<div class="result-nums">
<span class="result-num red" id="result-num-red">0</span>
<span class="result-num white" id="result-num-white">0</span>
</div>
</div>
</div>
<button id="start-btn" onclick="startGame()">玉入れ開始!(※音が出ます)</button>
<div class="controls">
<button class="guess-btn" id="btn-high" onclick="makeGuess('high')">赤(🔥)が多い</button>
<button class="guess-btn" id="btn-low" onclick="makeGuess('low')">白(❄️)が多い</button>
</div>
</div>
<script>
let currentScore = 0;
let timer;
let drumTimer, throwTimer;
let secondsLeft = 10;
let redHits = 0;
let whiteHits = 0;
let currentBeatTime = 700;
let currentAnimSpeed = 1.0;
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function initAudio() {
if (!audioCtx) audioCtx = new AudioContext();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playDrumBeat() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
gain.gain.setValueAtTime(1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
osc.start(audioCtx.currentTime);
osc.stop(audioCtx.currentTime + 0.5);
const volcano = document.getElementById('volcano');
volcano.classList.remove('beat-shake');
void volcano.offsetWidth;
volcano.classList.add('beat-shake');
}
function playThrowSound(isHit, speedFactor) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'triangle';
const baseFreq = isHit ? 400 : 300;
osc.frequency.setValueAtTime(baseFreq * speedFactor, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime((baseFreq + 200) * speedFactor, audioCtx.currentTime + 0.1);
gain.gain.setValueAtTime(0.05, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.start();
osc.stop(audioCtx.currentTime + 0.1);
}
function playExplosionSound() {
if (!audioCtx) return;
const bufferSize = audioCtx.sampleRate * 2;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(800, audioCtx.currentTime);
filter.frequency.exponentialRampToValueAtTime(40, audioCtx.currentTime + 1.5);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(1.5, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 2);
noise.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
noise.start();
const subOsc = audioCtx.createOscillator();
const subGain = audioCtx.createGain();
subOsc.connect(subGain);
subGain.connect(audioCtx.destination);
subOsc.type = 'sine';
subOsc.frequency.setValueAtTime(100, audioCtx.currentTime);
subOsc.frequency.exponentialRampToValueAtTime(10, audioCtx.currentTime + 1);
subGain.gain.setValueAtTime(2, audioCtx.currentTime);
subGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1.5);
subOsc.start();
subOsc.stop(audioCtx.currentTime + 1.5);
}
function updateBackground(score) {
const backgroundStates = ["sky-night", "sky-early-morning", "sky-sunrise", "sky-day", "sky-evening", "sky-sunset"];
const totalStates = backgroundStates.length;
const scorePerState = 1;
const totalSteps = Math.floor(score / scorePerState);
const cycleIndex = totalSteps % totalStates;
const cycleNumber = Math.floor(totalSteps / totalStates);
const gameArea = document.getElementById('game-area');
backgroundStates.forEach(cls => gameArea.classList.remove(cls));
gameArea.classList.add(backgroundStates[cycleIndex]);
const volcano = document.getElementById('volcano');
if (cycleNumber >= 1) {
volcano.classList.add('magma-active');
} else {
volcano.classList.remove('magma-active');
}
}
function startGame() {
initAudio();
redHits = 0; whiteHits = 0; secondsLeft = 10;
document.getElementById('score-num').innerText = currentScore;
document.getElementById('timer-num').innerText = secondsLeft;
currentBeatTime = Math.max(150, 700 - (currentScore * 30));
currentAnimSpeed = Math.max(0.2, 1.0 - (currentScore * 0.04));
const gameArea = document.getElementById('game-area');
const overlay = document.getElementById('result-overlay');
overlay.classList.remove('active');
// 残っている玉やパーティクルをすべて消去
document.querySelectorAll('.particle, .erupt-ball').forEach(p => p.remove());
document.getElementById('start-btn').disabled = true;
document.getElementById('start-btn').innerText = `玉入れ中... (スピード: ${currentScore + 1})`;
document.getElementById('btn-high').style.display = 'none';
document.getElementById('btn-low').style.display = 'none';
document.getElementById('result-text').innerText = "ドッカン!!";
gameArea.classList.add('throwing');
const ufo = document.getElementById('ufo');
ufo.className = '';
const patternNum = Math.floor(Math.random() * 5) + 1;
ufo.classList.add(`pattern-${patternNum}`);
updateBackground(currentScore);
drumLoop();
throwLoop();
timer = setInterval(() => {
secondsLeft--;
document.getElementById('timer-num').innerText = secondsLeft;
if (secondsLeft <= 0) endThrowing();
}, 1000);
}
function drumLoop() {
if (secondsLeft <= 0) return;
playDrumBeat();
drumTimer = setTimeout(drumLoop, currentBeatTime);
}
function throwLoop() {
if (secondsLeft <= 0) return;
let rCount = Math.floor(Math.random() * 3) + 1;
let wCount = Math.floor(Math.random() * 3) + 1;
for(let i=0; i<rCount; i++) createBall('red');
for(let i=0; i<wCount; i++) createBall('white');
throwTimer = setTimeout(throwLoop, currentBeatTime / 2);
}
function createBall(godType) {
const gameArea = document.getElementById('game-area');
const gameAreaRect = gameArea.getBoundingClientRect();
const ball = document.createElement('div');
ball.classList.add('ball', godType);
const isHit = Math.random() < 0.6;
if (isHit) {
ball.classList.add('hit');
if (godType === 'red') redHits++; else whiteHits++;
} else {
ball.classList.add('miss');
}
const speedFactor = 700 / currentBeatTime;
if(Math.random() > 0.5) playThrowSound(isHit, speedFactor);
const startX = godType === 'red' ? gameAreaRect.width * 0.15 : gameAreaRect.width * 0.85;
const startY = gameAreaRect.height - 80;
const targetX = gameAreaRect.width * 0.5;
const targetY = gameAreaRect.height * 0.35 + 27;
ball.style.left = startX + 'px';
ball.style.top = startY + 'px';
ball.style.setProperty('--speed', currentAnimSpeed + 's');
ball.style.setProperty('--tx', (targetX - startX) + 'px');
ball.style.setProperty('--ty', (targetY - startY) + 'px');
ball.style.setProperty('--mx', (Math.random() * 120 - 60) + 'px');
gameArea.appendChild(ball);
setTimeout(() => ball.remove(), currentAnimSpeed * 1000 + 200);
}
function endThrowing() {
clearInterval(timer);
clearTimeout(drumTimer);
clearTimeout(throwTimer);
const gameArea = document.getElementById('game-area');
gameArea.classList.remove('throwing');
document.getElementById('timer-num').innerText = "0";
document.getElementById('start-btn').innerText = "さあ、どっちが多いか予想せよ!";
document.getElementById('btn-high').style.display = 'block';
document.getElementById('btn-low').style.display = 'block';
}
// 演出用:細かい火花パーティクル
function createExplosionParticles() {
const gameArea = document.getElementById('game-area');
const gameAreaRect = gameArea.getBoundingClientRect();
const centerX = gameAreaRect.width * 0.5;
const centerY = gameAreaRect.height * 0.35 + 27;
for (let i = 0; i < 60; i++) {
const particle = document.createElement('div');
particle.classList.add('particle', Math.random() > 0.5 ? 'red' : 'white');
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * 500 + 100;
particle.style.setProperty('--start-x', centerX + 'px');
particle.style.setProperty('--start-y', centerY + 'px');
particle.style.setProperty('--end-x', (centerX + Math.cos(angle) * distance) + 'px');
particle.style.setProperty('--end-y', (centerY + Math.sin(angle) * distance) + 'px');
gameArea.appendChild(particle);
setTimeout(() => particle.remove(), 800);
}
}
// 実体:入った数だけ確実に大きな玉を吹き出させる
function createEruptionBalls() {
const gameArea = document.getElementById('game-area');
const gameAreaRect = gameArea.getBoundingClientRect();
const centerX = gameAreaRect.width * 0.5;
const centerY = gameAreaRect.height * 0.35 + 27;
const spawnEruptBall = (type, count) => {
for (let i = 0; i < count; i++) {
// 少しずつ時間差で吹き出す
setTimeout(() => {
const ball = document.createElement('div');
ball.classList.add('erupt-ball', type);
// 水平方向の散らばり
const targetX = centerX + (Math.random() * 400 - 200);
ball.style.setProperty('--start-x', centerX + 'px');
ball.style.setProperty('--start-y', centerY + 'px');
ball.style.setProperty('--end-x', targetX + 'px');
gameArea.appendChild(ball);
setTimeout(() => ball.remove(), 1500);
}, Math.random() * 400); // 0〜400msのランダム遅延
}
};
spawnEruptBall('red', redHits);
spawnEruptBall('white', whiteHits);
}
function makeGuess(guess) {
document.getElementById('btn-high').style.display = 'none';
document.getElementById('btn-low').style.display = 'none';
const gameArea = document.getElementById('game-area');
const overlay = document.getElementById('result-overlay');
playExplosionSound();
gameArea.classList.add('mega-shake');
setTimeout(() => gameArea.classList.remove('mega-shake'), 700);
// 爆発の演出(細かい火花 + 実際の玉の噴出)
createExplosionParticles();
createEruptionBalls();
document.getElementById('result-num-red').innerText = redHits;
document.getElementById('result-num-white').innerText = whiteHits;
let correct = false;
let isTie = (redHits === whiteHits);
if (isTie) {
correct = true;
document.getElementById('result-text').innerText = "奇跡の同点!";
} else if (guess === 'high' && redHits > whiteHits) {
correct = true;
} else if (guess === 'low' && whiteHits > redHits) {
correct = true;
}
// 玉が吹き出すのを見せるため、リザルト画面の表示を少し遅らせる
setTimeout(() => {
overlay.classList.add('active');
}, 600);
setTimeout(() => {
if (correct) {
currentScore++;
updateBackground(currentScore);
if (isTie) {
document.getElementById('start-btn').innerText = `無条件クリアだ! (連続${currentScore}回)`;
} else {
document.getElementById('start-btn').innerText = `大正解! 時間が進むぜ (連続${currentScore}回)`;
}
} else {
currentScore = 0;
updateBackground(currentScore);
document.getElementById('start-btn').innerText = "無念… 目が回ったか?やり直す";
}
document.getElementById('score-num').innerText = currentScore;
document.getElementById('start-btn').disabled = false;
}, 2000); // 結果画面が見えてからの待機時間を調整
}
</script>
</body>
</html>


■ 今日の学び

今回のゲームは、 人知を超えたスケールの世界観を、
あえて身近な「玉入れ」で表現してみました。(*^_^)ノ⌒ ゜Y

投げる側ではなく、
「どちらが多いかを見極める“カウント側”として参加する」という立場が、
不思議とこの壮大な儀式に巻き込まれているような感覚を生み出してくれました。

シンプルに“どちらが多いか”を当てるだけなのですが、
UFOが邪魔してきたりして、見ているとだんだん分からなくなってきます。

外れた玉の落ち方をヒントに予測すると、正解率が少し上がる気がしました。
とはいえ……10回連続クリアは至難の業です。(^^;)


■ 次回は、エネルギーを跳ね返す成長ゲームです。

跳ね返して壊すのではなく、
どんどん成長させていくのが目的の
アクション成長育成ゲームです。

次回もお楽しみに。(^v^)ノシ

次の実験はこちら(跳ね返し成長):「成長の庭」
※ふるふる育つ不思議な世界です。



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

コメントを残す

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

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

続きを読む