実験 #20:瞬きの間の判断力(アイドル猫の肉球サイン)

■20個目「アイドル猫の肉球サイン

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

 今回は、リズム感と集中力が試される「アイドル猫の肉球サイン」の制作実験です。

 どんなゲームかというと、アイドル猫がサインの代わりに
 肉球スタンプを色紙へポンポン押していきます。

 ところが、マネージャーはちょっと意地悪で、
 準備している色紙の中に先輩やベテランの色紙を混ぜ込んできます。

 押し間違わずに、できるだけ多くの色紙へスタンプを押していきましょう。

 押すタイミングが遅れたり、
 先輩たちの色紙にうっかり押してしまうと、
 マネージャーに怒られてゲームオーバー。(>_<、)

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

(^^*)html形式のメモアプリで動かせるゲーム作るよ。

 設定は、人気上昇中のアイドル猫。
 ファンへのサインの代わりに、似顔絵入り色紙へ肉球スタンプを押していきます。

 テンポよく自分の色紙へ押していくんだけど、
 意地悪なマネージャーが大物アイドル動物の色紙を混ぜてきます。

 犬やカワウソなど、ほかの大物アイドルの色紙が混じっているので、
 自分のじゃない色紙が出てきたときは押さずに待つこと。

 間違えて押すと「ごめんなさい!」と落ち込みゲームオーバー。
 逆に、自分の色紙なのに押さずにいるのもマネージャーに怒られてゲームオーバー。

 ゲームオーバーまでに押せた枚数がスコアになります。
 スコアランキングもあって、上位を目指すゲームです。
(AI)アイドル猫のサイン会、開幕ですね!
 HTML形式のメモアプリやブラウザでそのまま動かせる、
 シンプルながらも緊張感のあるミニゲームを作成しました。

(*^o^)肉球スタンプ押したら跡が残るようにして、
 押した色紙は左下へフェードアウト。
 「ぽん!」と音が鳴るといいね。テンポは秒針のような感じで。

 トラはレア出現にして、色紙感ももう少し出して。
背景はほのぼのした事務所の雰囲気にできるかな。
 文字の字体もかわいくしたい。

(AI)承知いたしました。
 アイドル猫ちゃんのサイン会、さらに迫力を増して、
 ファンサービスをたっぷり見せつける仕様に調整しました!

(*=v=)アイドル猫の割合は多めで、ほかの出現率は下げましょう。
 トラはさらにレアで。

(AI)出現比率を調整しました!
 アイドル猫(あなた)は約75%、他の動物はたまに登場、
 トラ様は滅多に現れない真のレアキャラです。

 これで、リズムよく「ぽん!」「ぽん!」とスタンプを押し続ける快感が
 さらに増しているはずです。

■ 実際の動作

 最初のアイデア相談のときに、AI が作ってくれた
 「アイドル猫の肉球スタンプ」ゲームです。
(今回は最初からイメージに近かったので、雰囲気の調整を中心に修正しています。)

 難易度は3種類から選べます。

 修正を進めて、サイズ感とやりごたえもアップ。
 自分(アイドル猫)の色紙にスタンプを押していきます。

 肉球スタンプを押すと、すぐに次の色紙が表示されます。

 自分の色紙じゃないのに間違えて押すと、マネージャーに怒られます。

 逆に、自分の色紙なのに押さずにいるときも、マネージャーに怒られてしまいます。

  ゲームの設定を相談しているときに、
 「色紙感ももう少し出して。背景はほのぼのした事務所の雰囲気にできるかな。」
 とお願いしたところ、AI が出してくれたイメージ画像がこちらです。

■ コードのポイント

 今回の記事では、AI と相談しながら作った
 「アイドル猫の肉球サイン」の HTML コードを公開します。

 ブラウザだけで動く、とてもシンプルなゲームです。

 PC の「メモ帳」(※Windowsの場合)を開き、
 下のコードをコピーして貼り付け、「neko.html」など好きな名前で保存してください。

 保存したファイルをブラウザで開くだけで遊べます。

【操作方法】
・難易度を選びます。
・自分の色紙だけに、マウスの左クリックでスタンプを押します。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>アイドル猫のサイン会 DX - ズームアップ版</title>
<link href="https://fonts.googleapis.com/css2?family=Kosugi+Maru&family=M+PLUS+Rounded+1c:wght@700&display=swap" rel="stylesheet">
<style>
:root {
--font-main: 'M PLUS Rounded 1c', 'Kosugi Maru', sans-serif;
--shikishi-size: 480px; /* さらにズームアップ */
}
body {
font-family: var(--font-main);
margin: 0;
overflow: hidden;
background-color: #fdf5e6;
background-image:
radial-gradient(#e6d5b8 15%, transparent 16%),
linear-gradient(to bottom, #f3e5ab, #e6d5b8);
background-size: 60px 60px, 100% 100%;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
user-select: none;
}
#game-area {
position: relative;
width: 100%;
max-width: 600px;
height: 800px;
display: flex;
flex-direction: column;
align-items: center;
}
.shikishi {
position: absolute;
width: var(--shikishi-size);
height: calc(var(--shikishi-size) * 1.1);
background: #fff;
border: 10px solid #f0e68c;
box-shadow: 10px 10px 30px rgba(0,0,0,0.3);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 2;
background-image: url('https://www.transparenttextures.com/patterns/natural-paper.png');
top: 60px;
}
.fade-out {
animation: slideOut 0.8s ease-in forwards;
pointer-events: none;
}
@keyframes slideOut {
to { transform: translate(-800px, 800px) rotate(-40deg); opacity: 0; }
}
#portrait { font-size: 220px; margin-bottom: 20px; } /* ズームアップ */
#idol-name {
font-size: 36px;
color: #444;
background: rgba(255,255,255,0.8);
padding: 8px 20px;
border-radius: 20px;
}
.stamp-mark {
position: absolute;
bottom: 30px;
right: 30px;
font-size: 180px; /* 特大ズームアップ */
color: rgba(216, 27, 96, 0.9);
animation: stampPop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
text-shadow: 0 0 15px rgba(216, 27, 96, 0.3);
}
@keyframes stampPop {
0% { transform: scale(3); opacity: 0; }
100% { transform: scale(1); opacity: 0.9; }
}
#ui-layer {
z-index: 5;
text-align: center;
margin-bottom: 10px;
}
#score-board { font-size: 40px; color: #8b4513; text-shadow: 1px 1px 0 white; }
#difficulty-label { font-size: 20px; color: #d81b60; font-weight: bold; }
.overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
.panel {
background: white;
padding: 40px;
border-radius: 30px;
text-align: center;
box-shadow: 0 12px 0 #d81b60;
width: 80%;
}
.btn-group { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; }
button {
font-family: var(--font-main);
padding: 15px;
font-size: 22px;
border: none;
border-radius: 50px;
cursor: pointer;
transition: 0.2s;
}
.btn-easy { background: #81c784; color: white; }
.btn-normal { background: #ffb74d; color: white; }
.btn-hard { background: #e57373; color: white; }
button:hover { opacity: 0.8; transform: scale(1.05); }
.hidden { display: none; }
</style>
</head>
<body>
<div id="game-area">
<div id="ui-layer">
<div id="score-board">スコア: 0 枚</div>
<div id="difficulty-label"></div>
</div>
<div id="shikishi-container"></div>
<div id="start-screen" class="overlay">
<div class="panel">
<h1 style="color:#d81b60; margin:0;">肉球サイン会🐾</h1>
<p>難易度を選んでね!</p>
<div class="btn-group">
<button class="btn-easy" onclick="startGame(1500, 'のんびり')">のんびり (1.5秒)</button>
<button class="btn-normal" onclick="startGame(1000, 'ふつう')">ふつう (1.0秒)</button>
<button class="btn-hard" onclick="startGame(700, '過密日程')">過密日程 (0.7秒)</button>
</div>
</div>
</div>
<div id="over-screen" class="overlay hidden">
<div class="panel">
<h2 id="over-msg" style="font-size:28px;">ごめんなさい!</h2>
<p>スコア: <span id="final-score" style="font-size:40px;color:#d81b60;">0</span></p>
<div id="rank-display" style="margin: 15px 0; text-align: left; font-size: 16px; border-top: 1px solid #ddd; padding-top: 10px;"></div>
<button style="background:#ff69b4; color:white; width:100%" onclick="location.reload()">タイトルへ戻る</button>
</div>
</div>
</div>
<script>
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playPon() {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(450, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(60, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.6, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.15);
}
const idols = [
{ char: '🐱', name: 'アイドル猫', isMe: true },
{ char: '🐶', name: '大物イヌ様', isMe: false },
{ char: '🦦', name: 'カワウソ師匠', isMe: false },
{ char: '🐰', name: 'バニー先輩', isMe: false },
{ char: '🐯', name: '激レア・トラ様', isMe: false }
];
let score = 0;
let gameActive = false;
let timer = null;
let currentTempo = 1000;
let currentDiffName = "";
function startGame(speed, diffName) {
score = 0;
currentTempo = speed;
currentDiffName = diffName;
gameActive = true;
document.getElementById('difficulty-label').textContent = `難易度: ${diffName}`;
document.getElementById('start-screen').classList.add('hidden');
document.getElementById('over-screen').classList.add('hidden');
updateScore();
spawnShikishi();
}
function spawnShikishi() {
if (!gameActive) return;
const container = document.getElementById('shikishi-container');
container.innerHTML = '';
let selected;
const rand = Math.random();
if (rand < 0.05) selected = idols[4]; // トラ 5%
else if (rand < 0.25) selected = idols[Math.floor(Math.random() * 3) + 1]; // その他 20%
else selected = idols[0]; // 自分 75%
const el = document.createElement('div');
el.className = 'shikishi';
el.innerHTML = `<div id="portrait">${selected.char}</div><div id="idol-name">${selected.name}</div>`;
el.onclick = () => handleInput(selected, el);
container.appendChild(el);
clearTimeout(timer);
timer = setTimeout(() => {
if (selected.isMe) {
endGame("マネージャー「自分の色紙だよ!ぼーっとしないで!」");
} else {
score++;
updateScore();
spawnShikishi();
}
}, currentTempo);
}
function handleInput(idol, el) {
if (!gameActive) return;
if (idol.isMe) {
playPon();
const s = document.createElement('div');
s.className = 'stamp-mark';
s.textContent = '🐾';
el.appendChild(s);
el.classList.add('fade-out');
score++;
updateScore();
clearTimeout(timer);
setTimeout(spawnShikishi, currentTempo * 0.4);
} else {
endGame(`「ごめんなさい!」${idol.name}さんのに押しちゃった…`);
}
}
function updateScore() {
document.getElementById('score-board').textContent = `スコア: ${score} 枚`;
}
function endGame(msg) {
gameActive = false;
clearTimeout(timer);
document.getElementById('over-msg').textContent = msg;
document.getElementById('final-score').textContent = score;
document.getElementById('over-screen').classList.remove('hidden');
showRanking();
}
function showRanking() {
const storageKey = `cat_rank_${currentDiffName}`;
let ranks = JSON.parse(localStorage.getItem(storageKey) || '[]');
ranks.push(score);
ranks.sort((a,b) => b-a);
ranks = ranks.slice(0, 3);
localStorage.setItem(storageKey, JSON.stringify(ranks));
document.getElementById('rank-display').innerHTML =
`<b>【${currentDiffName}】ランキング:</b><br>` +
ranks.map((s,i) => `${i+1}位: ${s}枚`).join('<br>');
}
</script>
</body>
</html>


■ 今日の学び

 今回のゲームは、集中力と瞬間的な判断力が必要なゲームとなりました。

 ほのぼのした世界観ですが、実際に遊んでみると意外とストイックな仕上がりです(^^;)
 かわいい見た目とのギャップがあって、作っていても面白い体験でした。

 作ったゲームの記事紹介は、間を飛ばし飛ばしなので、
 この猫のスタンプゲームでだいたい40個目くらいの作品になります。
 (紹介しやすいゲームから順番に出しているので、そのうち記事にしていきます。)

 このあたりのゲームから、AIさんを有料版に切り替えて作成しています。

 作っていると、最初のころに苦労していた部分が、
 今ではスムーズに形になっていくような気がします。(* ̄v ̄)~*
 ゲームのテンポ感や画面の雰囲気づくりも、以前より自然に整えられるようになりました。

 ただ気になるのが、AIさんの言葉遣いが、
 職人っぽくなったり、馴れ馴れしくなったりと、
キャラクターが安定しない時があることです。

 何やら設定があるようですが、そのままゲーム作りを続けていたので、
 記事を読んでいて「AIの返答の雰囲気が違うな」と気づく方もいるかもしれません。
 それも含めて“実験室らしさ”として楽しんでいただけたら嬉しいです。

 次回は、ミミズが主人公!
 「自然を感じられる」ゲームです。
 楽しんでいただければと思います。(*^v^)ノシ

次の実験はこちら(土壌改良ゲーム): 「ミミズの耕し」



 ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
 もし応援していただけたら、とても励みになります。

~このブログについて~

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

🌈初めての方へ
🌱このブログについて
🌳実験一覧
🍃プライバシーポリシー


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

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

続きを読む