🗂
素因数分解ゲーム
index.htmlで保存すると遊べます
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Divisor Challenge - Clarity Edition</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@400;600;800&display=swap');
:root {
--bg-dark: #0f172a;
--prime-color: #3b82f6;
--composite-color: #10b981;
--danger-color: #ef4444;
}
body {
font-family: 'Lexend', sans-serif;
background-color: var(--bg-dark);
color: white;
margin: 0;
overflow: hidden;
user-select: none;
}
/* 背景レイヤー (z-index: 0) */
.background-layer {
position: fixed;
inset: 0;
background: radial-gradient(circle at center, #1e293b 0%, #020617 100%);
z-index: 0;
}
/* エフェクトレイヤー (z-index: 10) */
#fx-canvas {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 10;
opacity: 0.8;
}
/* 数字表示レイヤー (z-index: 20) */
.main-display {
position: relative;
z-index: 20;
text-align: center;
pointer-events: none;
}
.number-text {
font-size: clamp(6rem, 25vw, 15rem);
font-weight: 800;
line-height: 1;
color: white;
/* 圧倒的な可読性のためのドロップシャドウ */
filter: drop-shadow(0 0 20px rgba(0,0,0,0.8));
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* 操作UIレイヤー (z-index: 30) */
.ui-layer {
position: relative;
z-index: 30;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2rem;
box-sizing: border-box;
}
.btn-game {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(8px);
border-bottom-width: 6px;
}
.btn-game:active {
transform: translateY(4px);
border-bottom-width: 2px;
}
.stack-indicator {
display: flex;
gap: 8px;
justify-content: center;
margin-top: 1rem;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #334155;
transition: all 0.3s ease;
}
.dot.active {
background: white;
box-shadow: 0 0 10px white;
transform: scale(1.2);
}
/* アニメーション */
@keyframes success-scale {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.animate-success {
animation: success-scale 0.3s ease-out;
}
.shake-ui {
animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both;
}
@keyframes shake {
10%, 90% { transform: translate3d(-2px, 0, 0); }
20%, 80% { transform: translate3d(4px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-6px, 0, 0); }
40%, 60% { transform: translate3d(6px, 0, 0); }
}
</style>
</head>
<body>
<div class="background-layer"></div>
<canvas id="fx-canvas"></canvas>
<div class="ui-layer">
<!-- 上部:HUD -->
<div class="flex justify-between items-start">
<div class="space-y-1">
<p class="text-blue-400 text-xs font-bold tracking-widest uppercase">Level</p>
<p id="level-val" class="text-4xl font-extrabold leading-none">1</p>
</div>
<div class="text-center">
<p class="text-slate-500 text-xs font-bold tracking-widest uppercase mb-2">Process Queue</p>
<div id="stack-dots" class="stack-indicator"></div>
</div>
<div class="text-right space-y-1">
<p class="text-blue-400 text-xs font-bold tracking-widest uppercase">Score</p>
<p id="score-val" class="text-4xl font-extrabold leading-none">0</p>
</div>
</div>
<!-- 中央:メイン数字 -->
<div class="main-display">
<div id="number-box" class="number-text">??</div>
<p id="question-text" class="text-slate-400 text-xl font-semibold tracking-[0.3em] mt-4 uppercase">Is this a Prime?</p>
</div>
<!-- 下部:操作ボタン -->
<div class="flex flex-col items-center gap-8 mb-4">
<p class="text-slate-500 text-sm font-medium">現在の最大範囲: <span id="limit-val">20</span></p>
<div class="grid grid-cols-2 gap-6 w-full max-w-2xl px-4">
<button onclick="answer(false)" class="btn-game bg-slate-800/80 hover:bg-slate-700 text-white py-8 rounded-2xl font-bold text-3xl border-slate-900 flex flex-col items-center">
<span>いいえ</span>
<span class="text-xs opacity-50 font-normal mt-1">合成数 (Composite)</span>
</button>
<button onclick="answer(true)" class="btn-game bg-blue-600 hover:bg-blue-500 text-white py-8 rounded-2xl font-bold text-3xl border-blue-800 flex flex-col items-center shadow-lg shadow-blue-500/20">
<span>はい</span>
<span class="text-xs opacity-70 font-normal mt-1">素数 (Prime)</span>
</button>
</div>
</div>
</div>
<!-- ゲームオーバー -->
<div id="gameover" class="hidden fixed inset-0 bg-slate-950/95 z-[100] flex flex-col items-center justify-center p-8 backdrop-blur-xl">
<h2 class="text-6xl font-extrabold text-white mb-2 italic">GAME OVER</h2>
<p id="over-reason" class="text-red-500 font-bold mb-8 tracking-widest"></p>
<div class="bg-slate-900 p-8 rounded-3xl text-center mb-8 w-full max-w-xs border border-white/10">
<p class="text-slate-400 text-sm mb-1">FINAL SCORE</p>
<p id="final-score" class="text-5xl font-extrabold text-white mb-4">0</p>
<p class="text-slate-400 text-sm mb-1">REACHED LEVEL</p>
<p id="final-level" class="text-2xl font-bold text-blue-400">1</p>
</div>
<button onclick="restart()" class="bg-white text-slate-950 px-12 py-4 rounded-full font-extrabold text-xl hover:bg-blue-500 hover:text-white transition-all transform hover:scale-110 active:scale-95 shadow-2xl">
RETRY
</button>
</div>
<script>
// --- 状態 ---
let game = {
level: 1,
score: 0,
current: { num: 0, factors: [] },
stack: [],
solved: 0
};
// --- パーティクル (背景で動作) ---
const canvas = document.getElementById('fx-canvas');
const ctx = canvas.getContext('2d');
let particles = [];
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.color = color;
this.size = Math.random() * 8 + 4;
const angle = Math.random() * Math.PI * 2;
const force = Math.random() * 12 + 5;
this.vx = Math.cos(angle) * force;
this.vy = Math.sin(angle) * force;
this.alpha = 1;
this.decay = Math.random() * 0.02 + 0.015;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.2; // Gravity
this.alpha -= this.decay;
this.size *= 0.96;
}
draw() {
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
function burst(color) {
for (let i = 0; i < 40; i++) {
particles.push(new Particle(window.innerWidth / 2, window.innerHeight / 2, color));
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles = particles.filter(p => p.alpha > 0);
particles.forEach(p => { p.update(); p.draw(); });
requestAnimationFrame(animate);
}
animate();
// --- 効果音 (Web Audio) ---
const audio = new (window.AudioContext || window.webkitAudioContext)();
function play(f, type, d, v = 0.1) {
const o = audio.createOscillator();
const g = audio.createGain();
o.type = type;
o.frequency.setValueAtTime(f, audio.currentTime);
g.gain.setValueAtTime(v, audio.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, audio.currentTime + d);
o.connect(g); g.connect(audio.destination);
o.start(); o.stop(audio.currentTime + d);
}
// --- ゲームエンジン ---
const numEl = document.getElementById('number-box');
const levelEl = document.getElementById('level-val');
const scoreEl = document.getElementById('score-val');
const limitEl = document.getElementById('limit-val');
const stackDotsEl = document.getElementById('stack-dots');
const uiLayer = document.querySelector('.ui-layer');
function getFactors(n) {
const f = [];
let d = 2;
let temp = n;
while (temp > 1) {
while (temp % d === 0) { f.push(d); temp /= d; }
d++;
if (d * d > temp) { if (temp > 1) f.push(temp); break; }
}
return f;
}
function restart() {
game = { level: 1, score: 0, current: null, stack: [], solved: 0 };
document.getElementById('gameover').classList.add('hidden');
spawnNew();
render();
}
function spawnNew() {
const limit = 20 + (game.level - 1) * 20;
const n = Math.floor(Math.random() * (limit - 1)) + 2;
game.current = { num: n, factors: getFactors(n) };
game.stack = [];
}
function render() {
numEl.innerText = game.current.num;
levelEl.innerText = game.level;
scoreEl.innerText = game.score;
limitEl.innerText = 20 + (game.level - 1) * 20;
// スタック表示
stackDotsEl.innerHTML = '';
const maxVisible = Math.max(game.stack.length, 5);
for (let i = 0; i < maxVisible; i++) {
const dot = document.createElement('div');
dot.className = `dot ${i < game.stack.length ? 'active' : ''}`;
stackDotsEl.appendChild(dot);
}
}
function answer(isUserPrime) {
const isActuallyPrime = game.current.factors.length === 1;
if (isUserPrime === isActuallyPrime) {
// Correct
const bonus = isActuallyPrime ? 100 : 200;
game.score += bonus;
// 視覚・音響演出 (数字の後ろで弾ける)
burst(isActuallyPrime ? '#3b82f6' : '#10b981');
play(isActuallyPrime ? 600 : 400, 'sine', 0.2);
numEl.classList.remove('animate-success');
void numEl.offsetWidth;
numEl.classList.add('animate-success');
if (!isActuallyPrime) {
// 分解
const mid = Math.ceil(game.current.factors.length / 2);
const f1 = game.current.factors.slice(0, mid);
const f2 = game.current.factors.slice(mid);
if (f2.length > 0) game.stack.push({ num: f2.reduce((a,b)=>a*b, 1), factors: f2 });
if (f1.length > 0) game.stack.push({ num: f1.reduce((a,b)=>a*b, 1), factors: f1 });
setTimeout(next, 150);
} else {
if (game.stack.length > 0) setTimeout(next, 150);
else cleared();
}
} else {
fail();
}
}
function next() {
if (game.stack.length > 0) {
game.current = game.stack.pop();
render();
} else {
cleared();
}
}
function cleared() {
game.solved++;
if (game.solved >= 2) {
game.level++;
game.solved = 0;
}
spawnNew();
render();
play(800, 'sine', 0.3, 0.05);
}
function fail() {
play(100, 'sawtooth', 0.5, 0.2);
uiLayer.classList.add('shake-ui');
setTimeout(() => uiLayer.classList.remove('shake-ui'), 400);
const reason = game.current.factors.length === 1 ? "それは素数でした" : "それは割り切れます";
document.getElementById('over-reason').innerText = `${game.current.num} は ${reason}`;
document.getElementById('final-score').innerText = game.score;
document.getElementById('final-level').innerText = game.level;
document.getElementById('gameover').classList.remove('hidden');
}
window.onload = restart;
</script>
</body>
</html>
Discussion