🎮ArcadeLab

数字华容道

by SonicWizard80
781 lines22.8 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数字华容道</title>
<style>
  :root {
    --bg-top: #f5f9ff;
    --bg-bottom: #d9e6f4;
    --accent: #4f8cff;
    --accent-soft: #e8f0ff;
    --tile: #ffffff;
    --text: #2c3e50;
    --text-muted: #8a97a8;
    --correct: #d7f3e3;
    --correct-text: #1f9d63;
    --shadow: 0 4px 12px rgba(60, 90, 140, 0.12);
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }

  body {
    position: relative;
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
    background:
      radial-gradient(1100px 760px at 12% -8%, rgba(120, 185, 255, 0.42), transparent 60%),
      radial-gradient(950px 720px at 105% 8%, rgba(140, 210, 255, 0.38), transparent 55%),
      radial-gradient(900px 760px at 50% 115%, rgba(165, 205, 245, 0.40), transparent 58%),
      linear-gradient(160deg, var(--bg-top), var(--bg-bottom));
    color: var(--text);
    -webkit-user-select: none;
    user-select: none;
  }

  /* 漂浮的柔光光斑 */
  body::before,
  body::after {
    content: '';
    position: fixed;
    border-radius: 50%;
    filter: blur(64px);
    opacity: 0.55;
    pointer-events: none;
    z-index: 0;
  }
  body::before {
    width: 360px;
    height: 360px;
    top: -90px;
    left: -70px;
    background: rgba(150, 195, 255, 0.6);
    animation: drift1 18s ease-in-out infinite;
  }
  body::after {
    width: 320px;
    height: 320px;
    bottom: -80px;
    right: -60px;
    background: rgba(185, 220, 255, 0.6);
    animation: drift2 24s ease-in-out infinite;
  }

  @keyframes drift1 {
    0%, 100% { transform: translate(0, 0) scale(1); }
    50% { transform: translate(46px, 34px) scale(1.12); }
  }
  @keyframes drift2 {
    0%, 100% { transform: translate(0, 0) scale(1); }
    50% { transform: translate(-40px, -28px) scale(1.16); }
  }

  .app {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    padding: 24px;
  }

  h1 {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: 2px;
  }

  .modes {
    display: flex;
    gap: 6px;
    padding: 5px;
    background: rgba(255, 255, 255, 0.6);
    border-radius: 13px;
    box-shadow: var(--shadow);
  }
  button.mode {
    padding: 7px 18px;
    font-size: 0.9rem;
    font-weight: 600;
    letter-spacing: 0;
    color: var(--text-muted);
    background: transparent;
    border-radius: 9px;
    box-shadow: none;
  }
  button.mode:hover {
    transform: none;
    box-shadow: none;
    background: var(--accent-soft);
  }
  button.mode.active,
  button.mode.active:hover {
    color: #fff;
    background: var(--accent);
  }

  .stats {
    display: flex;
    gap: 16px;
    align-items: center;
  }

  .stat {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 18px;
    background: rgba(255, 255, 255, 0.7);
    border-radius: 14px;
    box-shadow: var(--shadow);
    font-size: 0.95rem;
    color: var(--text-muted);
  }

  .stat b {
    font-size: 1.15rem;
    font-variant-numeric: tabular-nums;
    color: var(--text);
    min-width: 46px;
    text-align: center;
  }

  .best {
    font-size: 0.85rem;
    color: var(--text-muted);
  }
  .best b {
    color: var(--accent);
    font-weight: 700;
  }

  .hint-used {
    display: none;
    font-size: 0.82rem;
    color: #e08a00;
  }

  #board {
    position: relative;
    background: rgba(120, 150, 190, 0.14);
    border-radius: 18px;
    box-shadow: inset 0 2px 8px rgba(60, 90, 140, 0.10);
  }

  .tile {
    position: absolute;
    top: 0;
    left: 0;
    transform: translate(var(--tx, 0px), var(--ty, 0px));
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--tile);
    border-radius: 12px;
    box-shadow: var(--shadow);
    font-size: 1.6rem;
    font-weight: 600;
    color: var(--text);
    cursor: default;
    transition: transform 0.18s cubic-bezier(0.25, 0.8, 0.35, 1);
    will-change: transform;
  }

  .tile.movable {
    cursor: pointer;
  }

  .tile.movable:hover {
    background: var(--accent-soft);
    color: var(--accent);
    transform: translate(var(--tx), var(--ty)) scale(1.05);
  }

  .tile.correct {
    background: var(--correct);
    color: var(--correct-text);
  }

  button {
    padding: 12px 28px;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: 1px;
    color: #fff;
    background: var(--accent);
    border: none;
    border-radius: 14px;
    cursor: pointer;
    box-shadow: 0 6px 16px rgba(79, 140, 255, 0.35);
    transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
  }

  button:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(79, 140, 255, 0.4); }
  button:active { transform: translateY(0); }

  .actions { display: flex; gap: 12px; }

  button.secondary {
    color: var(--accent);
    background: #fff;
    box-shadow: 0 4px 12px rgba(60, 90, 140, 0.15);
  }
  button.secondary:hover {
    background: var(--accent-soft);
    box-shadow: 0 8px 18px rgba(60, 90, 140, 0.2);
  }

  .note { font-size: 0.8rem; color: var(--text-muted); }

  /* 提示高亮:脉冲光晕(只动 box-shadow,不影响定位 transform) */
  .tile.hint-glow {
    animation: hintPulse 0.9s ease-in-out infinite;
    z-index: 2;
  }
  @keyframes hintPulse {
    0%, 100% { box-shadow: 0 0 0 3px var(--accent), 0 0 14px rgba(79, 140, 255, 0.35); }
    50%      { box-shadow: 0 0 0 6px var(--accent), 0 0 30px rgba(79, 140, 255, 0.7); }
  }

  /* 胜利遮罩 */
  #overlay {
    position: fixed;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(30, 45, 70, 0.35);
    backdrop-filter: blur(3px);
    z-index: 10;
  }
  #overlay.show { display: flex; }

  /* 提示警告弹窗 */
  #warn-overlay {
    position: fixed;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(30, 45, 70, 0.35);
    backdrop-filter: blur(3px);
    z-index: 10;
  }
  #warn-overlay.show { display: flex; }

  .warn-card .actions { justify-content: center; }
  .warn-text {
    color: var(--text-muted);
    margin: 0 0 20px;
    max-width: 260px;
  }

  .card {
    background: #fff;
    border-radius: 20px;
    padding: 32px 40px;
    text-align: center;
    box-shadow: 0 20px 50px rgba(30, 45, 70, 0.3);
    animation: pop 0.35s cubic-bezier(0.2, 1.4, 0.4, 1);
  }
  .card .emoji { font-size: 3rem; }
  .card h2 { margin: 12px 0 6px; }

  .stars {
    font-size: 1.6rem;
    letter-spacing: 6px;
    color: #ffb020;
    margin: 4px 0 14px;
  }

  .summary {
    display: flex;
    gap: 12px;
    justify-content: center;
    margin-bottom: 16px;
  }
  .sum-item {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 12px 22px;
    background: #f5f8fc;
    border-radius: 12px;
    min-width: 104px;
  }
  .sum-label { font-size: 0.78rem; color: var(--text-muted); }
  .sum-value { font-size: 1.5rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }

  .record {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 20px;
    font-size: 0.9rem;
    color: var(--text-muted);
  }
  .rec-line { display: flex; align-items: center; gap: 8px; }
  .rec-line b { color: var(--text); margin-left: auto; font-variant-numeric: tabular-nums; }
  .badge {
    display: none;
    font-size: 0.65rem;
    font-weight: 700;
    color: #fff;
    background: var(--correct-text);
    padding: 2px 7px;
    border-radius: 999px;
    letter-spacing: 0.5px;
  }

  .hint-note {
    display: none;
    font-size: 0.82rem;
    color: #e08a00;
    margin: -8px 0 16px;
  }

  @keyframes pop {
    from { transform: scale(0.7); opacity: 0; }
    to { transform: scale(1); opacity: 1; }
  }
</style>
</head>
<body>
  <div class="app">
    <h1>数字华容道</h1>

    <div class="modes">
      <button class="mode" data-size="3">3×3</button>
      <button class="mode active" data-size="4">4×4</button>
      <button class="mode" data-size="5">5×5</button>
    </div>

    <div class="stats">
      <div class="stat">步数 <b id="steps">0</b></div>
      <div class="stat">用时 <b id="time">00:00</b></div>
    </div>

    <div class="best" id="best"></div>
    <div class="hint-used" id="hint-used">💡 已使用提示,本局成绩不计入最佳</div>

    <div id="board"></div>

    <div class="actions">
      <button id="hint-btn" class="secondary">💡 提示</button>
      <button id="restart">新 一 局</button>
    </div>
    <div class="note">点击空白格相邻的数字滑动 · 方向键也可操作</div>
  </div>

  <div id="overlay">
    <div class="card">
      <div class="emoji">🎉</div>
      <h2>恭喜通关!</h2>
      <div class="stars" id="stars">★★★</div>
      <div class="summary">
        <div class="sum-item">
          <span class="sum-label">本局步数</span>
          <span class="sum-value" id="sum-steps">0</span>
        </div>
        <div class="sum-item">
          <span class="sum-label">本局用时</span>
          <span class="sum-value" id="sum-time">00:00</span>
        </div>
      </div>
      <div class="record">
        <div class="rec-line"><span>最少步数</span><b id="rec-steps">—</b><span class="badge" id="badge-steps">NEW</span></div>
        <div class="rec-line"><span>最短用时</span><b id="rec-time">—</b><span class="badge" id="badge-time">NEW</span></div>
      </div>
      <p class="hint-note" id="hint-note">💡 本局使用了提示,未计入最佳成绩</p>
      <button id="again">再来一局</button>
    </div>
  </div>

  <div id="warn-overlay">
    <div class="card warn-card">
      <div class="emoji">⚠️</div>
      <h2>确定使用提示?</h2>
      <p class="warn-text">使用提示后,本局成绩将不再计入最佳记录。</p>
      <div class="actions">
        <button id="warn-cancel" class="secondary">取消</button>
        <button id="warn-confirm">确认使用</button>
      </div>
    </div>
  </div>

<script>
(function () {
  let SIZE = 4;                   // 当前模式:3 / 4 / 5
  const GAP = 6;                  // 格子间距(px)
  let CELL = 0;                   // 单格边长(px),按模式与窗口计算

  const boardEl = document.getElementById('board');
  const stepsEl = document.getElementById('steps');
  const timeEl = document.getElementById('time');
  const overlayEl = document.getElementById('overlay');
  const bestEl = document.getElementById('best');

  // ---------- 最佳成绩(按模式分别存本地) ----------
  function loadBest() {
    const blank = { 3: { steps: null, time: null }, 4: { steps: null, time: null }, 5: { steps: null, time: null } };
    try {
      const raw = localStorage.getItem('huarongdao_best');
      if (raw) {
        const d = JSON.parse(raw);
        for (const k of ['3', '4', '5']) {
          if (d[k]) blank[k] = { steps: d[k].steps ?? null, time: d[k].time ?? null };
        }
      }
    } catch (e) { /* 忽略损坏数据 */ }
    return blank;
  }
  function saveBest() {
    try { localStorage.setItem('huarongdao_best', JSON.stringify(best)); } catch (e) {}
  }
  let best = loadBest();

  function renderBest() {
    const b = best[SIZE];
    const st = b.steps === null ? '—' : b.steps + ' 步';
    const tm = b.time === null ? '—' : fmt(b.time);
    bestEl.innerHTML = '🏆 最佳成绩:' + st + ' · ' + tm;
  }

  // 棋盘状态:0 表示空白
  let board = [];
  let steps = 0;
  let startTime = null;   // 首次移动开始计时
  let timerId = null;
  let solved = false;
  let usedHint = false;   // 本局是否用过提示(用过则不计入最佳)
  let hintWarned = false; // 本局是否已弹过「使用提示」警告
  let lastMoved = -1;     // 打乱时避免来回无效移动
  let tileEls = {};       // 数字 -> DOM 元素

  // ---------- 尺寸计算 ----------
  function computeCell() {
    const maxCell = SIZE === 3 ? 112 : SIZE === 4 ? 96 : 76;
    const avail = Math.min(window.innerWidth, 460) - 40;
    CELL = Math.min(maxCell, Math.floor((avail - GAP * (SIZE - 1)) / SIZE));
    boardEl.style.width = boardEl.style.height = (CELL * SIZE + GAP * (SIZE - 1)) + 'px';
  }

  // ---------- 初始化 DOM 元素 ----------
  function buildTiles() {
    boardEl.innerHTML = '';
    tileEls = {};
    const fontSize = Math.round(CELL * (SIZE === 3 ? 0.55 : SIZE === 4 ? 0.5 : 0.4));
    for (let n = 1; n < SIZE * SIZE; n++) {
      const el = document.createElement('div');
      el.className = 'tile';
      el.textContent = n;
      el.style.width = el.style.height = CELL + 'px';
      el.style.fontSize = fontSize + 'px';
      el.addEventListener('click', () => moveTile(n));
      boardEl.appendChild(el);
      tileEls[n] = el;
    }
  }

  // 根据棋盘状态更新每个数字格子的位置
  function render() {
    for (let n = 1; n < SIZE * SIZE; n++) {
      const idx = board.indexOf(n);
      const row = Math.floor(idx / SIZE);
      const col = idx % SIZE;
      const tx = col * (CELL + GAP);
      const ty = row * (CELL + GAP);
      const el = tileEls[n];
      el.style.setProperty('--tx', tx + 'px');
      el.style.setProperty('--ty', ty + 'px');
      el.classList.toggle('correct', idx === n - 1);       // 数字 n 正确位置为 n-1
      el.classList.toggle('movable', isAdjacent(n));
    }
  }

  function isAdjacent(n) {
    const a = board.indexOf(n);
    const e = board.indexOf(0);
    const ar = Math.floor(a / SIZE), ac = a % SIZE;
    const er = Math.floor(e / SIZE), ec = e % SIZE;
    return Math.abs(ar - er) + Math.abs(ac - ec) === 1;
  }

  // ---------- 移动 ----------
  function moveTile(n) {
    if (solved || !isAdjacent(n)) return;
    const a = board.indexOf(n);
    const e = board.indexOf(0);
    [board[a], board[e]] = [board[e], board[a]];

    if (steps === 0) startTimer();
    steps++;
    stepsEl.textContent = steps;

    Object.values(tileEls).forEach(el => el.classList.remove('hint-glow'));
    render();
    checkWin();
  }

  // ---------- 打乱(保证有解:从已解状态做大量合法移动) ----------
  function shuffle() {
    board = Array.from({ length: SIZE * SIZE }, (_, i) => (i + 1) % (SIZE * SIZE)); // 已解状态 [1..N, 0]
    lastMoved = -1;
    const shuffleMoves = SIZE * SIZE * 60;
    for (let i = 0; i < shuffleMoves; i++) {
      const empty = board.indexOf(0);
      const er = Math.floor(empty / SIZE), ec = empty % SIZE;
      const candidates = [];
      [[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
        const r = er + dr, c = ec + dc;
        if (r >= 0 && r < SIZE && c >= 0 && c < SIZE) {
          const v = board[r * SIZE + c];
          if (v !== lastMoved) candidates.push(r * SIZE + c);
        }
      });
      // 若都被排除则允许回退,保证仍有移动
      if (candidates.length === 0) {
        [[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
          const r = er + dr, c = ec + dc;
          if (r >= 0 && r < SIZE && c >= 0 && c < SIZE) candidates.push(r * SIZE + c);
        });
      }
      const pick = candidates[Math.floor(Math.random() * candidates.length)];
      lastMoved = board[pick];
      [board[empty], board[pick]] = [board[pick], board[empty]];
    }
    // 极低概率打乱后恰为已解,重新打乱
    if (board.every((v, i) => v === (i + 1) % (SIZE * SIZE))) shuffle();
  }

  // ---------- 计时 ----------
  function startTimer() {
    startTime = Date.now();
    timerId = setInterval(() => {
      const s = Math.floor((Date.now() - startTime) / 1000);
      timeEl.textContent = fmt(s);
    }, 200);
  }
  function stopTimer() {
    if (timerId) { clearInterval(timerId); timerId = null; }
  }
  function fmt(s) {
    const m = String(Math.floor(s / 60)).padStart(2, '0');
    const ss = String(s % 60).padStart(2, '0');
    return m + ':' + ss;
  }

  // ---------- 提示(曼哈顿距离 + 线性冲突的一步前瞻) ----------
  function heuristic(b) {
    let dist = 0;
    for (let i = 0; i < SIZE * SIZE; i++) {
      const v = b[i];
      if (v === 0) continue;
      const tr = Math.floor((v - 1) / SIZE), tc = (v - 1) % SIZE;
      const cr = Math.floor(i / SIZE), cc = i % SIZE;
      dist += Math.abs(tr - cr) + Math.abs(tc - cc);
    }
    // 行线性冲突:同目标行内两个数字相对顺序颠倒
    for (let r = 0; r < SIZE; r++) {
      for (let c1 = 0; c1 < SIZE; c1++) {
        const v1 = b[r * SIZE + c1];
        if (v1 === 0 || Math.floor((v1 - 1) / SIZE) !== r) continue;
        for (let c2 = c1 + 1; c2 < SIZE; c2++) {
          const v2 = b[r * SIZE + c2];
          if (v2 === 0 || Math.floor((v2 - 1) / SIZE) !== r) continue;
          if ((v1 - 1) % SIZE > (v2 - 1) % SIZE) dist += 2;
        }
      }
    }
    // 列线性冲突:同目标列内两个数字相对顺序颠倒
    for (let c = 0; c < SIZE; c++) {
      for (let r1 = 0; r1 < SIZE; r1++) {
        const v1 = b[r1 * SIZE + c];
        if (v1 === 0 || (v1 - 1) % SIZE !== c) continue;
        for (let r2 = r1 + 1; r2 < SIZE; r2++) {
          const v2 = b[r2 * SIZE + c];
          if (v2 === 0 || (v2 - 1) % SIZE !== c) continue;
          if (Math.floor((v1 - 1) / SIZE) > Math.floor((v2 - 1) / SIZE)) dist += 2;
        }
      }
    }
    return dist;
  }

  let lastHintN = -1;

  // 束搜索(beam search):多步前瞻,比单步贪心更强
  function beamHint() {
    if (solved) return -1;
    const cfg = SIZE === 3 ? { depth: 6, width: 600 }
              : SIZE === 4 ? { depth: 4, width: 400 }
              :             { depth: 3, width: 250 };
    let beams = [{ state: board.slice(), first: null }];
    for (let d = 0; d < cfg.depth; d++) {
      const next = [];
      for (const node of beams) {
        const s = node.state;
        const empty = s.indexOf(0);
        const er = Math.floor(empty / SIZE), ec = empty % SIZE;
        [[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dr, dc]) => {
          const r = er + dr, c = ec + dc;
          if (r < 0 || r >= SIZE || c < 0 || c >= SIZE) return;
          const idx = r * SIZE + c;
          const ns = s.slice();
          [ns[empty], ns[idx]] = [ns[idx], ns[empty]];
          next.push({ state: ns, h: heuristic(ns), first: node.first ?? s[idx] });
        });
      }
      next.sort((a, b) => a.h - b.h);
      const seen = new Set();
      beams = [];
      for (const node of next) {
        const key = node.state.join(',');
        if (seen.has(key)) continue;
        seen.add(key);
        beams.push(node);
        if (beams.length >= cfg.width) break;
      }
      if (!beams.length) break;
    }
    if (!beams.length) return -1;
    beams.sort((a, b) => a.h - b.h);
    // 优先排除「撤销上一步提示」,减少来回循环
    const diff = beams.filter(b => b.first !== lastHintN && b.first !== null);
    const pool = diff.length ? diff : beams;
    const chosen = pool[0].first;
    if (chosen !== null) lastHintN = chosen;
    return chosen ?? -1;
  }

  function showHint() {
    if (solved) return;
    if (!hintWarned) {
      document.getElementById('warn-overlay').classList.add('show');
      return;
    }
    doHint();
  }

  function doHint() {
    const n = beamHint();
    if (n < 0) return;
    usedHint = true;
    document.getElementById('hint-used').style.display = 'block';
    Object.values(tileEls).forEach(el => el.classList.remove('hint-glow'));
    tileEls[n].classList.add('hint-glow');
  }

  // ---------- 胜利判定 + 通关结算 ----------
  function checkWin() {
    if (board.every((v, i) => v === (i + 1) % (SIZE * SIZE))) {
      solved = true;
      stopTimer();
      const s = Math.floor((Date.now() - startTime) / 1000);
      const b = best[SIZE];
      let brokeSteps = false, brokeTime = false;
      if (!usedHint) {
        brokeSteps = b.steps === null || steps < b.steps;
        brokeTime = b.time === null || s < b.time;
        if (brokeSteps) b.steps = steps;
        if (brokeTime) b.time = s;
        saveBest();
      }

      // 填充结算面板
      document.getElementById('sum-steps').textContent = steps;
      document.getElementById('sum-time').textContent = fmt(s);
      document.getElementById('rec-steps').textContent = b.steps + ' 步';
      document.getElementById('rec-time').textContent = fmt(b.time);
      document.getElementById('badge-steps').style.display = brokeSteps ? 'inline-block' : 'none';
      document.getElementById('badge-time').style.display = brokeTime ? 'inline-block' : 'none';
      document.getElementById('hint-note').style.display = usedHint ? 'block' : 'none';
      document.getElementById('stars').textContent = rating(steps);

      renderBest();
      overlayEl.classList.add('show');
    }
  }

  // 星级评价:按步数划分(阈值为经验值,可自行调整)
  function rating(st) {
    let a, b;
    if (SIZE === 3) { a = 35; b = 60; }
    else if (SIZE === 4) { a = 120; b = 200; }
    else { a = 350; b = 600; }
    if (st <= a) return '★★★';
    if (st <= b) return '★★☆';
    return '★☆☆';
  }

  // ---------- 新游戏 ----------
  function newGame() {
    stopTimer();
    steps = 0;
    startTime = null;
    solved = false;
    usedHint = false;
    hintWarned = false;
    lastHintN = -1;
    stepsEl.textContent = '0';
    timeEl.textContent = '00:00';
    overlayEl.classList.remove('show');
    document.getElementById('hint-used').style.display = 'none';
    renderBest();
    computeCell();
    buildTiles();
    shuffle();
    render();
  }

  // ---------- 键盘方向键 ----------
  document.addEventListener('keydown', (e) => {
    const empty = board.indexOf(0);
    const er = Math.floor(empty / SIZE), ec = empty % SIZE;
    const dir = { ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1] }[e.key];
    if (!dir) return;
    e.preventDefault();
    const r = er + dir[0], c = ec + dir[1];
    if (r >= 0 && r < SIZE && c >= 0 && c < SIZE) moveTile(board[r * SIZE + c]);
  });

  // 窗口尺寸变化时重新计算格子大小(保留当前模式,不重载)
  window.addEventListener('resize', () => {
    computeCell();
    buildTiles();
    render();
  });

  document.getElementById('restart').addEventListener('click', newGame);
  document.getElementById('again').addEventListener('click', newGame);
  document.getElementById('hint-btn').addEventListener('click', showHint);
  document.getElementById('warn-confirm').addEventListener('click', () => {
    hintWarned = true;
    document.getElementById('warn-overlay').classList.remove('show');
    doHint();
  });
  document.getElementById('warn-cancel').addEventListener('click', () => {
    document.getElementById('warn-overlay').classList.remove('show');
  });

  document.querySelectorAll('.mode').forEach(btn => {
    btn.addEventListener('click', () => {
      document.querySelectorAll('.mode').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      SIZE = parseInt(btn.dataset.size, 10);
      newGame();
    });
  });

  // ---------- 启动 ----------
  newGame();
})();
</script>
</body>
</html>

Game Source: 数字华容道

Creator: SonicWizard80

Libraries: none

Complexity: complex (781 lines, 22.8 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-sonicwizard80" to link back to the original. Then publish at arcadelab.ai/publish.