未命名 TrQaUoedit icon

创建者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
index.html
            
            <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>汉诺塔 - 自由层数版</title>
    <style>
        :root {
            --primary: #2196F3;
            --bg: #f5f7fa;
            --card: #ffffff;
            --pole: #795548;
            --base: #5d4037;
            --text: #333;
        }

        * { box-sizing: border-box; touch-action: manipulation; -webkit-tap-highlight-color: transparent; }

        body {
            font-family: system-ui, -apple-system, sans-serif;
            background: var(--bg);
            color: var(--text);
            margin: 0;
            padding: 10px;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
        }

        h1 { margin: 10px 0; font-size: 1.4rem; text-align: center; }

        .panel {
            background: var(--card);
            padding: 15px;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
            width: 100%;
            max-width: 600px;
            margin-bottom: 15px;
        }

        .settings-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
            gap: 10px;
            margin-bottom: 15px;
        }

        .field { display: flex; flex-direction: column; gap: 4px; }
        .field label { font-size: 0.8rem; color: #666; font-weight: 600; }
        
        input, select {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 16px;
            width: 100%;
            background: #fff;
        }

        .btn-row { display: flex; gap: 10px; }
        
        button {
            flex: 1;
            padding: 12px;
            border: none;
            border-radius: 6px;
            font-size: 1rem;
            font-weight: 600;
            color: white;
            cursor: pointer;
            transition: filter 0.2s;
        }
        button:active { filter: brightness(0.9); }
        button:disabled { opacity: 0.5; cursor: not-allowed; }

        .btn-start { background: var(--primary); }
        .btn-step { background: #4CAF50; }
        .btn-reset { background: #f44336; }

        .game-box {
            width: 100%;
            max-width: 600px;
            background: var(--card);
            border-radius: 12px;
            padding: 10px;
            margin-bottom: 15px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
        }

        .towers {
            display: flex;
            justify-content: space-around;
            align-items: flex-end;
            height: 300px;
            border-bottom: 6px solid var(--base);
            position: relative;
            overflow: hidden;
        }

        .tower {
            position: relative;
            width: 32%;
            height: 100%;
            display: flex;
            flex-direction: column-reverse;
            align-items: center;
        }

        .pole {
            position: absolute;
            bottom: 0;
            width: 6px;
            height: 90%;
            background: var(--pole);
            border-radius: 3px 3px 0 0;
            z-index: 0;
        }

        .disk {
            border-radius: 8px;
            border: 1px solid rgba(0,0,0,0.1);
            z-index: 1;
            box-shadow: 0 1px 2px rgba(0,0,0,0.1);
        }

        .label {
            position: absolute;
            bottom: -22px;
            font-size: 0.8rem;
            color: #888;
            font-weight: bold;
        }

        .log-box {
            width: 100%;
            max-width: 600px;
            height: 140px;
            background: var(--card);
            border-radius: 12px;
            padding: 10px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 0.85rem;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
        }

        .log-item {
            padding: 3px 0;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
        }
        .step-num { color: var(--primary); font-weight: bold; margin-right: 8px; white-space: nowrap; }
        .step-msg { text-align: right; }

        @media (max-width: 600px) {
            .towers { height: 45vw; min-height: 180px; }
            .settings-grid { grid-template-columns: 1fr 1fr; }
            .field:nth-child(3) { grid-column: span 2; }
            h1 { font-size: 1.2rem; }
            button { padding: 10px; font-size: 0.9rem; }
        }
    </style>
</head>
<body>

    <h1>汉诺塔演示</h1>

    <div class="panel">
        <div class="settings-grid">
            <div class="field">
                <label>层数 (≥2)</label>
                <input type="number" id="inpCount" value="3" min="2">
            </div>
            <div class="field">
                <label>速度 (ms/步)</label>
                <input type="number" id="inpSpeed" value="500" min="1" step="50">
            </div>
            <div class="field">
                <label>模式</label>
                <select id="inpMode">
                    <option value="auto">自动播放</option>
                    <option value="manual">手动单步</option>
                </select>
            </div>
        </div>
        <div class="btn-row">
            <button class="btn-start" id="btnStart">开始</button>
            <button class="btn-step" id="btnStep" disabled>下一步</button>
            <button class="btn-reset" id="btnReset">重置</button>
        </div>
    </div>

    <div class="game-box">
        <div class="towers" id="gameArea">
            <div class="tower" id="t0"><div class="pole"></div><div class="label">A</div></div>
            <div class="tower" id="t1"><div class="pole"></div><div class="label">B</div></div>
            <div class="tower" id="t2"><div class="pole"></div><div class="label">C</div></div>
        </div>
    </div>

    <div class="log-box" id="logBox"></div>

<script>
    const state = {
        towers: [[], [], []],
        pos: [],          // 每个盘子当前所在柱子索引
        idx: 0n,          // 当前步数(BigInt支持超大步数)
        total: 0n,        // 总步数
        timer: null,
        running: false,
        n: 3
    };

    const $ = id => document.getElementById(id);
    const dom = {
        t: [$('t0'), $('t1'), $('t2')],
        log: $('logBox'),
        area: $('gameArea'),
        start: $('btnStart'),
        step: $('btnStep'),
        reset: $('btnReset'),
        count: $('inpCount'),
        speed: $('inpSpeed'),
        mode: $('inpMode')
    };

    // ---------- 初始化 ----------
    function init() {
        stop();
        let n = parseInt(dom.count.value);
        if (isNaN(n) || n < 2) n = 3;
        dom.count.value = n;
        
        state.n = n;
        state.towers = [[], [], []];
        state.pos = new Array(n + 1).fill(0);
        state.idx = 0n;
        state.total = (1n << BigInt(n)) - 1n;

        for (let i = n; i >= 1; i--) state.towers[0].push(i);

        render();
        clearLog();
        addLog('Info', `已就绪 (${n}层, 共 ${state.total.toString()} 步)`);
        updateBtns();
    }

    // ---------- 实时计算下一步 ----------
    function getDisk(k) {
        let d = 1;
        let t = k;
        while ((t & 1n) === 0n) {
            t >>= 1n;
            d++;
        }
        return d;
    }

    function doStep() {
        if (state.idx >= state.total) {
            finish();
            return false;
        }

        const k = state.idx + 1n;
        const disk = getDisk(k);
        const from = state.pos[disk];
        const diff = BigInt(state.n - disk);
        const clockwise = (diff & 1n) === 1n;
        const to = clockwise ? (from + 1) % 3 : (from + 2) % 3;

        state.towers[from].pop();
        state.towers[to].push(disk);
        state.pos[disk] = to;

        state.idx = k;
        render();

        const names = ['A', 'B', 'C'];
        addLog(`#${k.toString()}`, `${names[from]} → ${names[to]}`);
        return true;
    }

    // ---------- 自动播放 ----------
    function loop() {
        if (!state.running) return;
        const cont = doStep();
        if (!cont) {
            stop();
            return;
        }
        const speed = Math.max(1, parseInt(dom.speed.value) || 500);
        state.timer = setTimeout(loop, speed);
    }

    function startAuto() {
        if (state.running) return;
        if (state.idx >= state.total) {
            alert('已完成,请点击重置');
            return;
        }
        state.running = true;
        updateBtns();
        loop();
    }

    function stop() {
        state.running = false;
        clearTimeout(state.timer);
        updateBtns();
    }

    function finish() {
        stop();
        addLog('Done', '🎉 移动完成!');
        setTimeout(() => alert(`恭喜!完成 ${state.total.toString()} 步`), 50);
    }

    // ---------- 渲染 ----------
    function render() {
        const h = dom.area.clientHeight;
        const maxH = Math.floor((h - 10) / state.n);
        const diskH = Math.max(1, Math.min(25, maxH));

        dom.t.forEach((el, i) => {
            const pole = el.querySelector('.pole');
            const lbl = el.querySelector('.label');
            el.innerHTML = '';
            el.appendChild(pole);
            el.appendChild(lbl);

            state.towers[i].forEach(size => {
                const d = document.createElement('div');
                d.className = 'disk';
                const w = 20 + (size / state.n) * 70;
                d.style.width = `${w}%`;
                d.style.height = `${diskH}px`;
                const hueSpan = state.n > 10 ? 120 : 200;
                const hue = 200 + (size / state.n) * hueSpan;
                d.style.backgroundColor = `hsl(${hue}, 70%, 60%)`;
                el.appendChild(d);
            });
        });
    }

    // ---------- UI ----------
    function updateBtns() {
        const isManual = dom.mode.value === 'manual';
        const done = state.idx >= state.total;
        dom.start.disabled = state.running || done;
        dom.step.disabled = state.running || !isManual || done;
        dom.count.disabled = state.running;
        dom.start.textContent = state.running ? '运行中...' : '开始';
    }

    function addLog(tag, msg) {
        const div = document.createElement('div');
        div.className = 'log-item';
        div.innerHTML = `<span class="step-num">${tag}</span><span class="step-msg">${msg}</span>`;
        dom.log.prepend(div);
    }

    function clearLog() { dom.log.innerHTML = ''; }

    // ---------- 事件 ----------
    dom.start.onclick = () => dom.mode.value === 'auto' ? startAuto() : doStep();
    dom.step.onclick = doStep;
    dom.reset.onclick = init;   // 重置随时可用,无确认弹窗
    dom.mode.onchange = () => { stop(); updateBtns(); };

    // 层数改变立即生效(防抖500ms)
    let debounce;
    dom.count.addEventListener('input', () => {
        clearTimeout(debounce);
        debounce = setTimeout(init, 500);
    });
    dom.count.addEventListener('change', init);

    let rTimer;
    window.addEventListener('resize', () => {
        clearTimeout(rTimer);
        rTimer = setTimeout(render, 200);
    });

    init();
</script>
</body>
</html>
        
编辑器加载中
预览
控制台