■53個目「遠心分離シミュレーター」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回のテーマはずばり、「遠心分離」です!
実験室にある、あのギュイーンと回る機械ですね。(=v=*)
今回用意した7色の液体には、
それぞれ異なる「比重(重さ)」が設定されています。
これらを好きな割合で混ぜ合わせると、
一つの新しい色の液体になりますが……
ここからがシミュレーションの本番です。
遠心分離機にセットして高速回転させると、物理の法則に従って、
重い液体から順番に外側(底)へと沈んでいきます。
混ざり合っていた色が、回転の力でじわじわと綺麗な層に分かれていく様子を、ブラウザ上で再現しました。
ゲームのルールは、左側の試験管にある「謎の混合液」を見て、
元の7色がどんな割合で入っていたのかを推理するというもの。
「この重さの液体がこれくらい沈んでいるから、たぶんあの色が……」
そんなふうに、白衣を着た研究者になった気分で楽しんでみてくださいね。
■ AIとのやりとりで面白かった部分
■ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲーム。
遠心分離機を再現したゲームを作ってみようと思います。
7色の液体(色ごとに比重の差がある)を好きな分量で混ぜると、
絵具のように一つの新しい色になります。
それを遠心分離器に入れて起動すると……
入れた割合に応じて、試験管の中で色が分離していきます。
AI: いいですね、実験室のマスターになった気分で楽しめそうです。
比重の違う液体が、回転によってじわじわと層に分かれていく様子は、
見ていてどこか癖になる快感があります。
HTML5 の Canvas API を使って、液体の混合(色の合成)と遠心力による分離をシミュレートするゲームを作成しました。
みー:(=_=*) 分離した色の層が見たいので、ストップボタンがほしいです。
クイズモードで「この混合物の割合を当てるゲーム」もおもしろそう。
当たった色と量によって点数制にして、
混合液の色から予想の色を15回クリック。
ひとつ正解で7点、満点は105点。
15回クリックできたらスタートボタンで分離開始、
分離後に正解発表。というのはどうかな。
AI: 素晴らしいアイデアですね!
「混ざった色から中身を当てる」というのは、
色彩感覚と推理力が試される面白いゲーム性です。
「正解」と「予測」の試験管が並んでいるので、
分離した後に色の層の厚みを比べることで、
どれくらい合っていたか一目でわかります。
みー:(つv<`) 正解と予測の試験管の向きが同じ方向に向いてるよ。
遠心分離機なので、回転の外側に試験管の底が向くようにするよ。
AI: 申し訳ありません、遠心力の物理法則を無視した配置になっていました。
ご指摘の通り、遠心分離機では回転の 外側 に向かって力がかかるため、
試験管の底は回転の中心から外向きにならなければなりません。
〈何度か修正のやり取りがあり、ようやく仕上がりました。(^^;)〉
AI: 今度こそ、試験管の底が外周を向き、重い色が外側に沈む 動きになっているはずです。
止まる直前にスッと水平位置に戻る動きも、
心地よく仕上がっていると思いますので、ぜひお試しください。
■ 実際の動作
こちらが、AIが最初に出してくれたシミュレーターの画面です。

(画像:最初にAIが作ってくれたゲームの画像)
こちらが、仕上がり版のスタート画面です。
左側に「混合された液体」が表示されているので、
その色を目指して、15回色を選んでクリックし、予測を作っていきます。

(画像:スタート画面)
調合が完了したら、STARTボタンを押します。

(画像:調合完了)
画像ではわかりにくいですが、この時点で試験管はぐるぐる回っています。

(画像:遠心分離中)
STOPボタンを押すと、いよいよ結果発表!
得点と、正解の配合が表示されます。

(画像:分離完了と結果発表)
■ コードのポイント
今回の記事では、AI と相談しながら作った「遠心分離シミュレーター」の HTML コードを公開します。
このコードをそのまま保存するだけで、あなたのPCでもすぐに遊べるようになります。
ブラウザだけで動く、とてもシンプルなゲームなので、気軽に試してみてくださいね。
▼ 遊ぶまでの手順
1. PC の「メモ帳」(※Windowsの場合)や、
Mac の「テキストエディット」など、テキストエディタを開きます。
2. 下のコードをコピーして貼り付けます。
3. 「ensinbunri.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
4. 保存したファイルをブラウザで開けば、すぐに遊べます。
■ 遊び方・操作説明
それでは、このラボでの実験手順(操作方法)をご説明しますね!(^_^)
① お題の液体を観察しよう!
ゲームが始まると、左側の試験管に
「正解(お題)」となる混合液が表示されます。
「この色、何色と何色が混ざっているのかな?」と、
まずはじっくり観察してみてください。(=v=*)
② 15回クリックして調合!
画面下にある7色のボタンを使って、自分なりに配合を推理します。
ボタンを合計 15回 クリックして、右側の試験管に液体を注入してください。
(※途中で「間違えたかも…」と思っても、どんな層ができるか最後まで楽しんでみてくださいね!)
③ 遠心分離、スタート!
15回選び終わると、「分離開始(START)」ボタンが押せるようになります。
ポチッと押すと、ギュイーン!と高速回転が始まりますよー!(゚v゜*)
④ 停止して結果発表!
回転の様子を眺めたら、「停止(STOP)」ボタンを押してください。
機械がピタッと水平に止まり、
遠心力で重さごとに分かれた色の層が現れます。
左がお題、右があなたの作った液体です。 層の厚みを見比べてみましょう!
⑤ 点数(スコア)を確認!
最後に、あなたの推理がどれくらい合っていたかが 105点満点 で表示されます。
遊ぶたびに正解の色は変わるので、目指せ、色彩と物理のマスター!
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>Centrifuge Quiz - 色彩推理ゲーム (完全物理修正版)</title> <style> body { background: #1a1a1a; color: white; font-family: 'Helvetica Neue', Arial, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; } canvas { background: #2c3e50; border-radius: 50%; box-shadow: 0 0 50px rgba(0,0,0,0.8); border: 8px solid #444; } .ui-panel { margin-top: 20px; background: #333; padding: 20px; border-radius: 15px; width: 450px; text-align: center; } .controls { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 15px 0; } button { padding: 10px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; color: white; transition: 0.2s; } button:disabled { opacity: 0.3; cursor: not-allowed; } .btn-color { border: 1px solid rgba(255,255,255,0.3); } .btn-action { background: #27ae60; grid-column: span 2; font-size: 1.1em; } .btn-stop { background: #c0392b; grid-column: span 2; font-size: 1.1em; } .status { font-size: 1.2em; margin-bottom: 10px; color: #f1c40f; min-height: 1.5em; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } .score-display { font-size: 1.5em; font-weight: bold; color: #2ecc71; margin-top: 10px; border-top: 1px solid #555; padding-top: 10px; } .info { font-size: 0.9em; color: #bdc3c7; margin-bottom: 5px; } </style></head><body> <div class="status" id="message">色を合計15回選んでください</div> <div class="info" id="clickCount">残り: 15 クリック</div> <canvas id="canvas" width="450" height="450"></canvas> <div class="ui-panel"> <div class="controls" id="colorButtons"></div> <div class="controls"> <button id="startBtn" class="btn-action" onclick="startCentrifuge()" disabled>分離開始 (START)</button> <button id="stopBtn" class="btn-stop" onclick="stopCentrifuge()" disabled>停止 (STOP)</button> </div> <button style="width:100%; background:#7f8c8d; margin-top: 10px;" onclick="initGame()">次の問題 (リセット)</button> <div id="resultArea"></div> </div><script>const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');const LIQUIDS = [ { id: 0, name: "赤", color: {r: 255, g: 0, b: 0}, density: 2.0 }, { id: 1, name: "橙", color: {r: 255, g: 128, b: 0}, density: 1.7 }, { id: 2, name: "黄", color: {r: 255, g: 255, b: 0}, density: 1.4 }, { id: 3, name: "緑", color: {r: 0, g: 255, b: 0}, density: 1.1 }, { id: 4, name: "水", color: {r: 0, g: 255, b: 255}, density: 0.8 }, { id: 5, name: "青", color: {r: 0, g: 0, b: 255}, density: 0.5 }, { id: 6, name: "紫", color: {r: 128, g: 0, b: 255}, density: 0.2 }];let targetRecipe = [];let playerRecipe = [];const totalClicks = 15;let isSpinning = false;let separationProgress = 0;let rotationAngle = 0;let rotationalSpeed = 0;let gamePhase = 'mixing';function initGame() { targetRecipe = Array(7).fill(0); playerRecipe = Array(7).fill(0); let count = 0; while(count < totalClicks) { targetRecipe[Math.floor(Math.random() * 7)]++; count++; } gamePhase = 'mixing'; separationProgress = 0; rotationAngle = 0; rotationalSpeed = 0; isSpinning = false; document.getElementById('message').innerText = "左の混合液を見て、中身を推理!"; document.getElementById('clickCount').innerText = `残り: ${totalClicks} クリック`; document.getElementById('startBtn').disabled = true; document.getElementById('stopBtn').disabled = true; document.getElementById('resultArea').innerHTML = ""; updateButtons();}function updateButtons() { const btnContainer = document.getElementById('colorButtons'); btnContainer.innerHTML = ''; LIQUIDS.forEach((l, i) => { const btn = document.createElement('button'); btn.className = 'btn-color'; btn.style.background = `rgb(${l.color.r},${l.color.g},${l.color.b})`; btn.innerText = playerRecipe[i] > 0 ? `${l.name}(${playerRecipe[i]})` : l.name; btn.onclick = () => addLiquid(i); btn.disabled = (gamePhase !== 'mixing' || getPlayerTotal() >= totalClicks); btnContainer.appendChild(btn); });}function getPlayerTotal() { return playerRecipe.reduce((a, b) => a + b, 0);}function addLiquid(index) { playerRecipe[index]++; const remaining = totalClicks - getPlayerTotal(); document.getElementById('clickCount').innerText = `残り: ${remaining} クリック`; if (remaining <= 0) { document.getElementById('message').innerText = "調合完了!分離を開始してください"; document.getElementById('startBtn').disabled = false; } updateButtons();}function startCentrifuge() { gamePhase = 'spinning'; isSpinning = true; document.getElementById('startBtn').disabled = true; document.getElementById('stopBtn').disabled = false; document.getElementById('message').innerText = "分離中... (高速回転中)";}function stopCentrifuge() { isSpinning = false; gamePhase = 'stopped'; document.getElementById('stopBtn').disabled = true; document.getElementById('message').innerText = "分離完了!結果発表"; calculateScore();}function calculateScore() { let matches = 0; for(let i=0; i<7; i++) { matches += Math.min(targetRecipe[i], playerRecipe[i]); } const score = matches * 7; let resHtml = `<div class="score-display">得点: ${score} / 105 点</div>`; resHtml += "<div style='margin-top:10px; font-size:0.9em;'>【正解の配合】<br>"; LIQUIDS.forEach((l, i) => { if(targetRecipe[i] > 0) resHtml += `${l.name}:<span style="color:#f1c40f">${targetRecipe[i]}</span> `; }); resHtml += "</div>"; document.getElementById('resultArea').innerHTML = resHtml;}function mixColors(recipe) { let r = 0, g = 0, b = 0, total = 0; recipe.forEach((count, i) => { r += LIQUIDS[i].color.r * count; g += LIQUIDS[i].color.g * count; b += LIQUIDS[i].color.b * count; total += count; }); return total === 0 ? {r:50, g:70, b:90} : { r: r/total, g: g/total, b: b/total };}function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); const centerX = canvas.width / 2; const centerY = canvas.height / 2; // ローター背景 ctx.fillStyle = "#34495e"; ctx.beginPath(); ctx.arc(centerX, centerY, 190, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = "#2c3e50"; ctx.lineWidth = 2; ctx.stroke(); ctx.save(); ctx.translate(centerX, centerY); // 回転・減速・水平スナップロジック if (isSpinning) { rotationalSpeed = Math.min(rotationalSpeed + 0.005, 0.25); if (separationProgress < 1) separationProgress += 0.003; rotationAngle += rotationalSpeed; } else { if (rotationalSpeed > 0) { rotationalSpeed = Math.max(rotationalSpeed - 0.002, 0); rotationAngle += rotationalSpeed; } else { // 完全停止時に水平(0度 または 180度)に滑らかに吸い付く const targetAngle = Math.round(rotationAngle / Math.PI) * Math.PI; rotationAngle += (targetAngle - rotationAngle) * 0.1; } } ctx.rotate(rotationAngle); // アーム(ローター) ctx.strokeStyle = "#95a5a6"; ctx.lineWidth = 20; ctx.lineCap = "round"; ctx.beginPath(); ctx.moveTo(-150, 0); ctx.lineTo(150, 0); ctx.stroke(); // 中心点 ctx.fillStyle = "#7f8c8d"; ctx.beginPath(); ctx.arc(0,0,30,0,Math.PI*2); ctx.fill(); // =============== 右側:予測(プレイヤー) =============== ctx.save(); ctx.translate(140, 0); // 試験管本体(-90度回転させることで底が外側(右)を向く) ctx.save(); ctx.rotate(-Math.PI / 2); drawTube(playerRecipe, separationProgress); ctx.restore(); // ラベルテキスト(回転させずに試験管の下に配置) ctx.fillStyle = "white"; ctx.font = "bold 14px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("予測 (あなた)", 0, 35); ctx.restore(); // =============== 左側:正解(お題) =============== ctx.save(); ctx.translate(-140, 0); // 試験管本体(+90度回転させることで底が外側(左)を向く) ctx.save(); ctx.rotate(Math.PI / 2); const targetProg = (gamePhase === 'mixing' ? 0 : separationProgress); drawTube(targetRecipe, targetProg); ctx.restore(); // ラベルテキスト(回転させずに試験管の下に配置) ctx.fillStyle = "white"; ctx.font = "bold 14px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("正解 (お題)", 0, 35); ctx.restore(); ctx.restore(); requestAnimationFrame(draw);}// 試験管単体の描画(0が底、上がマイナス座標)function drawTube(recipe, progress) { const tubeWidth = 34; const tubeHeight = 110; const liquidMaxHeight = 90; const mixed = mixColors(recipe); let currentY = 0; // 底の位置 // 液体 if (recipe.reduce((a,b)=>a+b, 0) > 0) { if (progress < 0.05) { ctx.fillStyle = `rgb(${mixed.r}, ${mixed.g}, ${mixed.b})`; ctx.beginPath(); ctx.roundRect(-tubeWidth/2 + 2, -liquidMaxHeight, tubeWidth - 4, liquidMaxHeight, [0, 0, 15, 15]); ctx.fill(); } else { // 比重でソート(重い順に底から積む) const sortedIndices = [...Array(7).keys()].sort((a, b) => LIQUIDS[b].density - LIQUIDS[a].density); sortedIndices.forEach((idx) => { const amt = recipe[idx]; if (amt > 0) { const layerH = (amt / totalClicks) * liquidMaxHeight; const r = mixed.r + (LIQUIDS[idx].color.r - mixed.r) * progress; const g = mixed.g + (LIQUIDS[idx].color.g - mixed.g) * progress; const b = mixed.b + (LIQUIDS[idx].color.b - mixed.b) * progress; ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; ctx.beginPath(); const yPos = currentY - layerH; // 一番底だけ丸みをつける const radii = (currentY === 0) ? [0, 0, 15, 15] : [0, 0, 0, 0]; ctx.roundRect(-tubeWidth/2 + 2, yPos, tubeWidth - 4, layerH + 0.5, radii); ctx.fill(); currentY -= layerH; } }); } } // ガラス枠 ctx.fillStyle = "rgba(255,255,255,0.15)"; ctx.beginPath(); ctx.roundRect(-tubeWidth/2, -tubeHeight, tubeWidth, tubeHeight, [0, 0, 17, 17]); ctx.fill(); ctx.strokeStyle = "rgba(255,255,255,0.6)"; ctx.lineWidth = 2; ctx.stroke();}initGame();draw();</script></body></html>
■ 今日の学び
今回は、#51 の「スライスと乾燥シミュレーター」でスライサーの回転を眺めているうちに、
「遠心分離も作ってみたいな」と思い立ったところから始まったゲームでした。
実験器具は昔から好きで、#25 でも書いたようにマグネティックスターラーは持っています。
ですが、さすがに遠心分離機までは持っていません。(^x^;)
最近は、器具の洗浄殺菌用の超音波洗浄機あたりがちょっと気になっていたりします…。
今回はまず“シミュレーション部分”から作り始めたのですが、
色を予測して調合し、最後に START を押して分析する瞬間のわくわく感がたまらなくて、 とても満足のいくゲームになりました。(⌒ v ⌒*)
ちなみに、満点と 0 点の両方を取ることにも成功しました。(` v ´ *)
■ 次回は、水中からの脱出ゲームです。
浮かび上がるをテーマに、#09 FALL FITの、
ゲームボーイ風の世界観で作ったゲームです。
水面めざして浮かんでいくゲームです。
次回もぜひ遊びに来てくださいね。(*^v^)ノシ
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






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