実験#117:月と花と花言葉(月相ルーレット)

■117個目 「月相ルーレット

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

今回は、夜空と植物がそっとつながるような、
少し不思議でやさしい世界をテーマにしたゲームを作ってみました。

その名も、「月相ルーレット – 16bit 幻想の花畑」です。

新月、三日月、満月……。

地平線の向こうで月がくるくると回り、
ストップを押した瞬間に天頂へすっと昇ります。

選ばれた月の満ち欠けに合わせて、
大地には色とりどりの花が咲き誇るんです。(=v=*)

今回こだわったのは、
懐かしさあふれる 16bitのドット絵レトロなピコピコ音

昔のゲーム機のように、月のクレーターの陰影や花びらの一枚一枚を、
細かなドットで丁寧に描いてみました。

月がカチカチと回る音や、
花が咲く瞬間に流れるピコピコとしたアルペジオは、
聞いているだけでどこかあたたかく、ほっとする気持ちになれます。

咲くお花は全部で12種類。

それぞれに素敵な「花言葉」が添えられているので、
今日はどんな言葉が咲くのかな……と、

ちょっとした占い気分で楽しんでいただけます。(^^)

どんな月が輝き、どんな花が咲くのか。
それはルーレットを回してからのお楽しみです。

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

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

みー:(^v^*) 今日もゲームを作ります。

HTMLで動くゲームで、月の満ち欠けをルーレットで表現したいんです。

新月は0、三日月1、間2、半月3、間4、手前5、満月6というイメージで、
地平線の中心を軸に、その奥で7種類の月が円を描くように回転します。

ストップを押したときに天頂に来た月が当たり目になるようにしたいので、
まずはこの部分を素敵に再現してみてください。

AI: 月の満ち欠けをモチーフにしたルーレット、
とても幻想的で素敵なアイデアですね!

地平線の奥で月が回転し、天頂に来た月が当たり目になる仕組みを、
HTML・CSS・JavaScriptで再現しました。

こだわりポイント

  • ストップ時に、最も近い月が天頂に吸い込まれるように止まる“スナップ”演出
  • 地平線より下の月が隠れるように配置し、星空の雰囲気を演出
  • 月の満ち欠けを自然な順番で円周上に配置
  • 回転中も月の影が正しい向きを保つように逆回転処理を追加

みー:(^_^) 狙い通りに止まりすぎるので、
スピードを少し上げましょう。

止まるときにゆっくり減速して、
最後のスナップはそのままにしたいです。

AI: 確かに、すぐ止まると目押しができてしまいますね。

回転スピードを上げ、
ストップ後に慣性でゆっくり減速するフェーズを追加しました。
十分に減速したあとでスナップが発動するように調整しています。

みー:(=v=) 月の満ち欠けは植物の成長にも影響すると聞きます。

ルーレットの結果によって中央に植物が伸びて、
月の種類に応じて12種類の花を咲かせるようにしましょう。

AI: とてもロマンチックな発想ですね!

ルーレットが止まった月に応じて茎が伸び、
12種類の花が咲くアニメーションを追加しました。

新月は「新芽」、満月は「ヒマワリ」のように、
月の状態に合わせた花を設定しています。

みー:( ̄v ̄) 続けて遊んでいると、ストップが押せなくなる時があります。

ついでに花言葉も出せるかな。

画面の両サイドを幻想的な花畑にできるかな。

AI: ボタンが押せなくなる不具合は、
再始動中の0.5秒間に多重クリックが起きてしまうことが原因でした。

再始動中はボタンをロックするように修正しました。

また、花言葉の表示と、画面両サイドに光る花畑の装飾を追加しました。

風に揺れる花や光の粒子をCSSアニメーションで表現しています。

みー:(*^o^)ノ 絵文字で表現している月を、
ドット絵で描くことはできそうかな。

図形の組み合わせでも大丈夫です。

AI: ドット絵は得意です!
外部画像を使わず、JavaScriptのCanvasで「8×8マスのピクセルアート」を
直接描く仕組みに変更しました。

レトロなドット感を出すために、CSSで pixelated 表示にしています。

満月にはクレーターの影も描き込んであります。

みー:(*^_^*) さらに細かいドット絵は難しくなるかな。
月と花を16×16で描ける?

AI: 仕組みは同じなので難しくはありませんが、
ドット数が増える分データ量は増えますね。

今回は解像度を「16×16」に引き上げて、
花びらの重なりや月の陰影まで表現できるようにしました。

キャンバスも少し大きくして、
細かなディテールが見えるように調整しています。

■ 実際の動作

まずは、AI が最初に出してくれたゲーム画面をご紹介します。
今回イメージしていた雰囲気にとても近かったので、
この状態を土台に仕上げていきました。

(画像:AIが最初に出してくれたゲーム画面)


こちらが、仕上げたあとのスタート画面です。
絵文字からドット絵へと変わり、月の陰影や色合いがより神秘的になりました。

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


そしてこちらは、仕上がりのゲーム画面です。
ドット絵の月が幻想的な感じです。

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


それぞれの花も、AI がドット絵で丁寧に描いてくれています。
花の種類は全部で 12 種類。
どんな花が咲くのか、ぜひルーレットを回して楽しんでみてくださいね。

(画像:ゲーム画面)




■ コードのポイント

今回の記事では、
AI と相談しながら作った月相ルーレット の HTML コードを公開します。

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

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

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

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

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

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

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

■ 遊び方・操作方法

操作はマウスクリック(スマホならタップ)だけの、
とても簡単なゲームです。(^_^)/

① 「スタート」を押す

画面下の「スタート」ボタンをポチッと押すと、
星空の下で月のルーレットが回り始めます。

カチカチと鳴る懐かしい音と一緒に、
どんな月が来るのかワクワクします。(=v=*)

② タイミングを合わせて「ストップ」

「ここだ!」と思ったタイミングで、もう一度ボタンを押してください。
ルーレットは少しずつゆっくりになり、
一番近い月にふわっと吸い込まれるように止まります。

③ 幻想的なお花と花言葉の誕生

月が天頂で止まると、その月の満ち欠けに合わせた「16bitのお花」が
大地からニョキッと咲きます。(´∀`)

ピコピコ♪という可愛い音色とともに、
お花の名前と素敵な花言葉が表示されます。

「もう一度回す」を押すと、お花がスッと引っ込み、
また新しいルーレットに挑戦できます。

全部で 12 種類のお花が隠れているので、
ぜひコンプリートを目指して楽しんでみてくださいね。(^^*)

今日はどんなお花が咲くでしょうか。
あなたのお気に入りの月とお花を見つけてみてください♪



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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>月相ルーレット - 16bit ドット絵版</title>
<style>
:root {
--sky-color: #0b0f19;
--ground-color: #05070a;
}
body {
margin: 0;
padding: 0;
background-color: #000;
color: #fff;
font-family: 'Helvetica Neue', Arial, sans-serif;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.scene {
position: relative;
width: 100vw;
height: 100vh;
background: linear-gradient(to bottom, #070b19 0%, #15234b 50%, var(--sky-color) 100%);
overflow: hidden;
}
/* 星空の背景 */
.stars {
position: absolute;
top: 0; left: 0; right: 0; bottom: 50%;
background-image:
radial-gradient(1px 1px at 20px 30px, #fff, rgba(0,0,0,0)),
radial-gradient(1px 1px at 40px 70px, #fff, rgba(0,0,0,0)),
radial-gradient(1px 1px at 90px 40px, #fff, rgba(0,0,0,0)),
radial-gradient(2px 2px at 160px 120px, #ddd, rgba(0,0,0,0));
background-repeat: repeat;
background-size: 200px 200px;
opacity: 0.6;
}
.ground {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50vh;
background: linear-gradient(to bottom, #0a0e17 0%, #000000 100%);
border-top: 1px solid #2a3b61;
z-index: 10;
}
.horizon-center {
position: absolute;
top: 50vh;
left: 50vw;
width: 10px;
height: 10px;
background: #fff;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 15px 5px rgba(255, 255, 255, 0.5);
z-index: 20;
}
/* 幻想的な花畑 */
.flower-field {
position: absolute;
bottom: 0;
width: 25vw;
height: 55vh;
z-index: 15;
pointer-events: none;
display: flex;
justify-content: center;
}
.flower-field.left { left: 0; }
.flower-field.right { right: 0; }
.field-item {
position: absolute;
font-size: 2rem;
filter: drop-shadow(0 0 8px rgba(255,255,255,0.4));
animation: sway 4s ease-in-out infinite alternate;
}
.glowing-particle {
position: absolute;
width: 4px;
height: 4px;
background: #fff;
border-radius: 50%;
box-shadow: 0 0 10px 3px rgba(255, 240, 150, 0.8);
opacity: 0;
animation: floatUp 8s ease-in infinite;
}
@keyframes sway {
0% { transform: rotate(-10deg) translateY(0); }
100% { transform: rotate(10deg) translateY(-5px); }
}
@keyframes floatUp {
0% { transform: translateY(0) scale(1); opacity: 0; }
20% { opacity: 0.8; }
80% { opacity: 0.5; }
100% { transform: translateY(-150px) scale(0.5); opacity: 0; }
}
/* 植物のコンテナ */
.plant-container {
position: absolute;
bottom: 50vh;
left: 50vw;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
height: 180px;
z-index: 25;
pointer-events: none;
}
/* 16x16ドットに合わせてサイズをアップ */
.flower {
width: 96px;
height: 96px;
opacity: 0;
transform: scale(0);
transition: opacity 0.5s ease-out, transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
margin-bottom: -5px;
filter: drop-shadow(0 0 12px rgba(255,255,255,0.6));
background-size: cover;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
}
.stem {
width: 8px;
height: 0;
background: linear-gradient(to right, #2e7d32, #4caf50, #2e7d32);
border-radius: 4px 4px 0 0;
transition: height 1s ease-in-out;
}
.plant-container.grown .stem { height: 90px; }
.plant-container.grown .flower {
opacity: 1;
transform: scale(1);
transition-delay: 1s;
}
/* ルーレット本体 */
.roulette-container {
position: absolute;
top: 50vh;
left: 50vw;
width: 0;
height: 0;
z-index: 5;
}
.roulette {
position: absolute;
top: 0; left: 0;
transform-origin: 0 0;
}
/* 月も16x16に合わせてサイズアップ */
.moon {
position: absolute;
width: 80px; height: 80px;
transform: translate(-50%, -50%);
filter: drop-shadow(0 0 15px rgba(244, 246, 255, 0.5));
}
.moon-inner {
display: block;
width: 100%;
height: 100%;
background-size: cover;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
}
/* UI部分 */
.ui {
position: absolute;
bottom: 5vh;
left: 50%;
transform: translateX(-50%);
z-index: 30;
text-align: center;
width: 100%;
}
button {
background: rgba(255, 255, 255, 0.1);
border: 2px solid #fff;
color: #fff;
padding: 15px 40px;
font-size: 1.2rem;
border-radius: 30px;
cursor: pointer;
backdrop-filter: blur(5px);
transition: all 0.3s ease;
letter-spacing: 2px;
}
button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.3);
box-shadow: 0 0 20px rgba(255,255,255,0.5);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button:active:not(:disabled) { transform: scale(0.95); }
#result {
margin-top: 15px;
font-size: 1.1rem;
line-height: 1.6;
text-shadow: 0 0 8px rgba(255,255,255,0.9);
height: 3.5rem;
}
.zenith-marker {
position: absolute;
top: 5vh; left: 50%;
transform: translateX(-50%);
width: 2px; height: 30px;
background: linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,0));
z-index: 20;
}
</style>
</head>
<body>
<div class="scene">
<div class="stars"></div>
<div class="zenith-marker"></div>
<div class="roulette-container">
<div class="roulette" id="roulette"></div>
</div>
<div class="ground"></div>
<div class="horizon-center"></div>
<div class="flower-field left" id="fieldLeft"></div>
<div class="flower-field right" id="fieldRight"></div>
<div class="plant-container" id="plantContainer">
<div class="flower" id="flower"></div>
<div class="stem"></div>
</div>
<div class="ui">
<button id="stopBtn">スタート</button>
<div id="result"></div>
</div>
</div>
<script>
// --- ドット絵の設計図(花:16x16マス) ---
const f_suzuran = [
"................",
".......w........",
"......www.......",
".....wwwww......",
".....wwwww..g...",
".....ww.ww.gw...",
"......w.w.gwww..",
"..........gwwww.",
".........gw.ww..",
".......GgG.w.w..",
"......GGgGG.....",
".....GGGgGGG....",
"........g.......",
"........g.......",
"........g.......",
"........g......."
];
const f_tulip = [
"......r..r......",
".....rr..rr.....",
"....rrrrrrrr....",
"....rrrrrrrr....",
"....RrrrrrrR....",
"....RRrrrrRR....",
".....RRRRRR.....",
".......g........",
".....G.g.G......",
"....GG.g.GG.....",
"...GGG.g.GGG....",
"...GG..g..GG....",
"....G..g..G.....",
".......g........",
".......g........",
".......g........"
];
const f_daisy = [
"................",
"......wwww......",
"....wwwwwwww....",
"...wwwwwwwwww...",
"...wwwwyywwww...",
"...wwwyyyywww...",
"...wwwwyywwww...",
"....wwwwwwww....",
"......wwww......",
".......g........",
".....g.g.g......",
"....gg.g.gg.....",
"...ggg.g.ggg....",
".......g........",
".......g........",
".......g........"
];
const f_sakura = [
"......P..P......",
".....pP..Pp.....",
"....pppppppp....",
"...pppppppppp...",
"...pppw..wppp...",
"..pppwwPPwwppp..",
"..ppppw..wpppp..",
"...pppppppppp...",
"....pppppppp....",
".....p....p.....",
"................",
"................",
"................",
"................",
"................",
"................"
];
const f_hibiscus = [
"....rrrrrr......",
"...rrrrrrrr.....",
"..rrrrrrrrrr....",
"..rrrRyRrrrr....",
"..rrrRyRrrrr....",
"..rrrrrrrrrr....",
"...rrrrrrrr.....",
".....rrrrr......",
".......r........",
"................",
"................",
"................",
"................",
"................",
"................",
"................"
];
const f_rose = [
".......rr.......",
".....rrrrrr.....",
"....rrRrrRrr....",
"...rrrrrrrrrr...",
"...rRrrrrrrRr...",
"...rrrrrrrrrr...",
"....rRRRRRRr....",
".....rRrrRr.....",
".......g........",
".....GgGgG......",
"....G..g..G.....",
".......g........",
".......g........",
".......g........",
".......g........",
".......g........"
];
const f_sunflower = [
".....yyYYyy.....",
"...yyYYYYYYyy...",
"..yyYbbbbbbYyy..",
".yyYYbbbbbbYYyy.",
".yyYYbbbbbbYYyy.",
".yyYYbbbbbbYYyy.",
".yyYYbbbbbbYYyy.",
"..yyYbbbbbbYyy..",
"...yyYYYYYYyy...",
".....yyggyy.....",
".......g........",
".....GgGgG......",
"....G..g..G.....",
".......g........",
".......g........",
".......g........"
];
const f_lotus = [
"................",
".......p........",
"......pPp.......",
".....ppPpp......",
"....pppPppp.....",
"....pppPppp.....",
"...ppppPpppp....",
".PppppppppppP...",
".GPPPPPPPPPPG...",
"..GGGGGGGGGG....",
"................",
"................",
"................",
"................",
"................",
"................"
];
const f_marigold = [
"................",
"......oOo.......",
"....ooOOOoo.....",
"...oooOOOooo....",
"..ooOoOoOoOoo...",
"..oooOOOOOooo...",
"...ooooooooo....",
".......g........",
".....g.g.g......",
"....gg.g.gg.....",
"...ggg.g.ggg....",
".......g........",
".......g........",
".......g........",
".......g........",
".......g........"
];
const f_hyacinth = [
".......U........",
"......uUu.......",
".....UuUuU......",
"......uUu.......",
".....UuUuU......",
"......uUu.......",
".....UuUuU......",
"......uUu.......",
".......g........",
".....GgGgG......",
"....GGgGgGG.....",
"...GGGgGgGGG....",
".......g........",
".......g........",
".......g........",
".......g........"
];
const f_forget = [
"................",
".......a........",
"......aaa.......",
".....aayaa......",
"......aaa.......",
"....a.....a.....",
"...aaa...aaa....",
"..aayaa.aayaa...",
"...aaa...aaa....",
"....a..g..a.....",
"......ggg.......",
".......g........",
".......g........",
".......g........",
".......g........",
".......g........"
];
const f_susuki = [
"........e.......",
".......ee.......",
"........ee......",
".........ee.....",
".........be.....",
".........b......",
"........gbg.....",
".......ggbbg....",
"......ggg.ggg...",
".....ggg...ggg..",
"....ggg.....ggg.",
"................",
"................",
"................",
"................",
"................"
];
const moonPhasesData = [
{ name: '新月', f_pattern: f_suzuran, flowerName: 'スズラン', meaning: '再び幸せが訪れる' },
{ name: '三日月', f_pattern: f_tulip, flowerName: 'チューリップ', meaning: '思いやり・愛の告白' },
{ name: '宵月', f_pattern: f_daisy, flowerName: 'デイジー', meaning: '純潔・無邪気' },
{ name: '上弦の月', f_pattern: f_sakura, flowerName: 'サクラ', meaning: '精神の美・優美' },
{ name: '十三夜', f_pattern: f_hibiscus, flowerName: 'ハイビスカス', meaning: '繊細な美・新しい恋' },
{ name: '待宵の月', f_pattern: f_rose, flowerName: 'バラ', meaning: '愛・情熱' },
{ name: '満月', f_pattern: f_sunflower, flowerName: 'ヒマワリ', meaning: '憧れ・あなただけを見つめる' },
{ name: '十六夜', f_pattern: f_lotus, flowerName: '蓮(ハス)', meaning: '清らかな心・神聖' },
{ name: '立待月', f_pattern: f_marigold, flowerName: 'マリーゴールド', meaning: '勇者・可憐な愛情' },
{ name: '下弦の月', f_pattern: f_hyacinth, flowerName: 'ヒヤシンス', meaning: '変わらぬ愛情' },
{ name: '有明の月', f_pattern: f_forget, flowerName: '忘れな草', meaning: '真実の愛・私を忘れないで' },
{ name: '三十日月', f_pattern: f_susuki, flowerName: 'ススキ', meaning: '活力・心が通じる' }
];
// --- ドット絵の設計図(月:16x16マス) ---
const p0 = [
".....dddddd.....",
"...dddddddddd...",
"..dddddddddddd..",
".dddddddddddddd.",
".dddddddddddddd.",
"dddddddddddddddd",
"dddddddddddddddd",
"dddddddddddddddd",
"dddddddddddddddd",
"dddddddddddddddd",
"dddddddddddddddd",
".dddddddddddddd.",
".dddddddddddddd.",
"..dddddddddddd..",
"...dddddddddd...",
".....dddddd....."
];
const p1 = [
".....dddddl.....",
"...ddddddddll...",
"..ddddddddddll..",
".ddddddddddddll.",
".ddddddddddddll.",
"dddddddddddddlll",
"dddddddddddddlll",
"dddddddddddddlll",
"dddddddddddddlll",
"dddddddddddddlll",
"dddddddddddddlll",
".ddddddddddddll.",
".ddddddddddddll.",
"..ddddddddddll..",
"...ddddddddll...",
".....dddddl....."
];
const p2 = [
".....ddddll.....",
"...ddddddllll...",
"..ddddddddllll..",
".ddddddddddllll.",
".ddddddddddllll.",
"ddddddddddllllll",
"ddddddddddllllll",
"ddddddddddllllll",
"ddddddddddllllll",
"ddddddddddllllll",
"ddddddddddllllll",
".ddddddddddllll.",
".ddddddddddllll.",
"..ddddddddllll..",
"...ddddddllll...",
".....ddddll....."
];
const p3 = [
".....ddllll.....",
"...ddddllllll...",
"..ddddddllllll..",
".ddddddllllllll.",
".ddddddllllllll.",
"ddddddddllllllll",
"ddddddddllllllll",
"ddddddddllllllll",
"ddddddddllllllll",
"ddddddddllllllll",
"ddddddddllllllll",
".ddddddllllllll.",
".ddddddllllllll.",
"..ddddddllllll..",
"...ddddllllll...",
".....ddllll....."
];
const p4 = [
".....dlllll.....",
"...ddllllllll...",
"..ddllllllllll..",
".ddllllllllllll.",
".ddllllllllllll.",
"ddllllllllllllll",
"ddllllllllllllll",
"ddllllllllllllll",
"ddllllllllllllll",
"ddllllllllllllll",
"ddllllllllllllll",
".ddllllllllllll.",
".ddllllllllllll.",
"..ddllllllllll..",
"...ddllllllll...",
".....dlllll....."
];
const p5 = [
".....llllll.....",
"...dlllllllll...",
"..dlllllllllll..",
".dlllllllllllll.",
".dlllllllllllll.",
"dlllllllllllllll",
"dlllllllllllllll",
"dlllllllllllllll",
"dlllllllllllllll",
"dlllllllllllllll",
"dlllllllllllllll",
".dlllllllllllll.",
".dlllllllllllll.",
"..dlllllllllll..",
"...dlllllllll...",
".....llllll....."
];
const p6 = [
".....llllll.....",
"...llclllllcc...",
"..llllccllllcc..",
".llllllcllllllcl.",
".llclllllcclllll.",
"lllclllllclllllc",
"llllllcllllllccc",
"lllllcccllllllll",
"lllllccllllcllll",
"llllllllllllclll",
"clllllccllllllll",
".cclllllclllllc.",
".clllllllllllll.",
"..llllllllllll..",
"...clllllccll...",
".....llllll....."
];
function mirror(pattern) {
return pattern.map(row => row.split('').reverse().join(''));
}
const pixelPatterns = [
p0, p1, p2, p3, p4, p5, p6,
mirror(p5), mirror(p4), mirror(p3), mirror(p2), mirror(p1)
];
// --- ドット絵ジェネレーター ---
function createPixelMoonImage(pattern) {
const canvas = document.createElement('canvas');
canvas.width = 16; canvas.height = 16;
const ctx = canvas.getContext('2d');
const colors = { '.': 'transparent', 'd': '#1a233a', 'l': '#f4f6ff', 'c': '#c4cbe5' };
pattern.forEach((row, y) => {
row.split('').forEach((char, x) => {
if (colors[char] && colors[char] !== 'transparent') {
ctx.fillStyle = colors[char];
ctx.fillRect(x, y, 1, 1);
}
});
});
return canvas.toDataURL();
}
function createPixelFlowerImage(pattern) {
const canvas = document.createElement('canvas');
canvas.width = 16; canvas.height = 16;
const ctx = canvas.getContext('2d');
const colors = {
'.': 'transparent',
'g': '#4caf50', 'G': '#2e7d32',
'w': '#ffffff',
'r': '#f44336', 'R': '#b71c1c',
'y': '#ffeb3b', 'Y': '#fbc02d',
'p': '#f48fb1', 'P': '#c2185b',
'b': '#795548',
'o': '#ff9800', 'O': '#e65100',
'U': '#9c27b0', 'u': '#e1bee7',
'a': '#81d4fa',
'e': '#d7ccc8'
};
pattern.forEach((row, y) => {
row.split('').forEach((char, x) => {
if (colors[char] && colors[char] !== 'transparent') {
ctx.fillStyle = colors[char];
ctx.fillRect(x, y, 1, 1);
}
});
});
return canvas.toDataURL();
}
const roulette = document.getElementById('roulette');
const stopBtn = document.getElementById('stopBtn');
const resultDisplay = document.getElementById('result');
const plantContainer = document.getElementById('plantContainer');
const flowerElement = document.getElementById('flower');
const radius = window.innerHeight * 0.4;
const totalMoons = 12;
const anglePerMoon = 360 / totalMoons;
const moonElements = [];
moonPhasesData.forEach((phase, index) => {
const angle = index * anglePerMoon;
const radian = (angle - 90) * (Math.PI / 180);
const x = radius * Math.cos(radian);
const y = radius * Math.sin(radian);
const moonWrapper = document.createElement('div');
moonWrapper.className = 'moon';
moonWrapper.style.left = `${x}px`;
moonWrapper.style.top = `${y}px`;
const moonInner = document.createElement('div');
moonInner.className = 'moon-inner';
moonInner.style.backgroundImage = `url(${createPixelMoonImage(pixelPatterns[index])})`;
moonWrapper.appendChild(moonInner);
roulette.appendChild(moonWrapper);
moonElements.push(moonInner);
});
function createFlowerField(containerId) {
const container = document.getElementById(containerId);
const flowers = ['🌸', '🌼', '🌷', '🌾', '🌿'];
for (let i = 0; i < 8; i++) {
const el = document.createElement('div');
el.className = 'field-item';
el.textContent = flowers[Math.floor(Math.random() * flowers.length)];
el.style.left = `${Math.random() * 80 + 10}%`;
el.style.bottom = `${Math.random() * 60}%`;
el.style.fontSize = `${Math.random() * 1.5 + 1}rem`;
el.style.opacity = Math.random() * 0.5 + 0.3;
el.style.animationDelay = `${Math.random() * 4}s`;
container.appendChild(el);
}
for (let i = 0; i < 15; i++) {
const p = document.createElement('div');
p.className = 'glowing-particle';
p.style.left = `${Math.random() * 100}%`;
p.style.bottom = `${Math.random() * 40}%`;
p.style.animationDelay = `${Math.random() * 8}s`;
p.style.animationDuration = `${Math.random() * 4 + 4}s`;
container.appendChild(p);
}
}
createFlowerField('fieldLeft');
createFlowerField('fieldRight');
let audioCtx = null;
function initAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playTick() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(800, audioCtx.currentTime);
gain.gain.setValueAtTime(0.03, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.05);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.05);
}
function playBloom() {
if (!audioCtx) return;
const time = audioCtx.currentTime;
const freqs = [523.25, 659.25, 783.99, 1046.50];
freqs.forEach((freq, i) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.05, time + i * 0.08);
gain.gain.exponentialRampToValueAtTime(0.001, time + i * 0.08 + 0.15);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(time + i * 0.08);
osc.stop(time + i * 0.08 + 0.15);
});
}
let currentRotation = 0;
const BASE_SPEED = 12;
let currentSpeed = BASE_SPEED;
let phase = 'initial';
let targetRotation = 0;
let animationId;
let lastTickIndex = -1;
function animate() {
if (phase === 'initial') return;
if (phase === 'spinning') {
currentRotation -= currentSpeed;
} else if (phase === 'stopping') {
currentSpeed *= 0.98;
currentRotation -= currentSpeed;
if (currentSpeed < 0.5) {
phase = 'snapping';
targetRotation = Math.round(currentRotation / anglePerMoon) * anglePerMoon;
}
} else if (phase === 'snapping') {
currentRotation += (targetRotation - currentRotation) * 0.1;
if (Math.abs(targetRotation - currentRotation) < 0.1) {
currentRotation = targetRotation;
phase = 'stopped';
showResult();
return;
}
}
if (currentRotation <= -360) {
currentRotation += 360;
if (phase === 'snapping') targetRotation += 360;
}
if (phase === 'spinning' || phase === 'stopping') {
let currentIndex = Math.floor(Math.abs(currentRotation) / anglePerMoon) % totalMoons;
if (currentIndex !== lastTickIndex) {
playTick();
lastTickIndex = currentIndex;
}
}
roulette.style.transform = `rotate(${currentRotation}deg)`;
moonElements.forEach(moon => {
moon.style.transform = `rotate(${-currentRotation}deg)`;
});
if (phase !== 'stopped') {
animationId = requestAnimationFrame(animate);
}
}
function showResult() {
let normalizedRotation = currentRotation % 360;
if (normalizedRotation > 0) normalizedRotation -= 360;
let winningIndex = Math.round(Math.abs(normalizedRotation) / anglePerMoon) % totalMoons;
const winningPhase = moonPhasesData[winningIndex];
flowerElement.style.backgroundImage = `url(${createPixelFlowerImage(winningPhase.f_pattern)})`;
plantContainer.classList.add('grown');
resultDisplay.innerHTML = `【${winningPhase.name}】が輝き、『${winningPhase.flowerName}』が咲きました。<br><span style="color:#ffd700; font-size: 0.9em;">花言葉:「${winningPhase.meaning}」</span>`;
setTimeout(() => {
playBloom();
}, 1000);
setTimeout(() => {
stopBtn.textContent = 'もう一度回す';
stopBtn.disabled = false;
}, 1500);
}
stopBtn.addEventListener('click', () => {
initAudio();
if (stopBtn.disabled) return;
if (phase === 'initial' || phase === 'stopped') {
stopBtn.disabled = true;
plantContainer.classList.remove('grown');
setTimeout(() => {
phase = 'spinning';
currentSpeed = BASE_SPEED;
resultDisplay.textContent = '';
stopBtn.textContent = 'ストップ';
stopBtn.disabled = false;
lastTickIndex = -1;
animate();
}, phase === 'initial' ? 0 : 500);
} else if (phase === 'spinning') {
phase = 'stopping';
stopBtn.disabled = true;
}
});
</script>
</body>
</html>


■ 今日の学び

今回のゲームは、「月の満ち欠けをルーレットにしてみよう」という、
とてもシンプルな発想からスタートしました。

最初は月やお花をすべて絵文字で表現していたのですが、
形になっていくにつれて
「もう少し世界観を深めたいな…」と感じるようになりました。

そこで思い切って、AI に「絵文字ではなく、
ドット絵で表現することってできるかな?」と相談してみたんです。

すると返ってきたのは、
とても頼もしい「できますよ!」という返事。( ✧Д✧)

その言葉に背中を押されて、このゲームの幻想的な世界観を、
すべて 16bit のドット絵で描いてもらうことになりました。

これまでのゲームでは、
絵文字や図形の組み合わせで表現することが多かったのですが、

今回ドット絵という手法を取り入れたことで、
私の中でゲーム表現の世界がふわっと広がったように感じています。(˘︶˘)

もちろん、プログラムで生成する AI のドット絵なので、
X(旧Twitter)で見かける職人さんのような芸術的なレベルには及びません。

それでも、外部の画像ファイルを使わず、
コードだけでレトロな世界観をブラウザに描き出せるというのは、
私にとってとても大きな学びでした。

そして実は、この「月相ルーレット」以降に作っているゲームでも、
さっそくこのドット絵表現をたくさん活用しています。₍₍ (ง ˘ω˘ )ว ⁾⁾

表現の幅が広がった「100のゲーム実験室」。
これからも AI と一緒に、少しずつできることを増やしながら、
楽しい実験を続けていきますね。

■ 次回は、海抜0mから月を目指す螺旋階段クリッカーゲームです。

「宇宙の絶景とやりこみ要素を楽しめるゲームを作りたい」
という思いから、 透明なチューブの中に続く螺旋階段を、
ひたすら上っていくクリッカーゲームを作ってみました。

連打でエネルギーを注ぎ込みながら、

ゆっくりと遠ざかっていく地球と、
少しずつ大きくなる月面を眺める旅。

高度が上がるごとに明かされる月の雑学や、

ドット絵ロボのパワーアップなど、

爽快感と達成感がぎゅっと詰まった、
やみつきになるゲームになっています。

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


次の実験はこちら
ゼロから月への軌跡
「LUNAR ASCENSION」




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

 

~このブログについて~

AIと一緒に遊び、
100の実験でゲームを育てるブログ

🌈初めての方へ
🌱このブログについて
🌳実験一覧
🍃プライバシーポリシー
🐣未記事ゲーム展示室


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

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

続きを読む