🗂

素因数分解ゲーム

に公開

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