Last Update: Mar.1,2025

JavaScript Sample

動作確認:○Firefox115(Win7) ○Chrome109(Win7)

■ 15パズルを解く(画像版)

最短手数の最大値は80手なのだそうです。





■ 80手が必要な配置(例)

■ ソース

document.addEventListener('DOMContentLoaded', () => {

  const tiles = document.querySelectorAll('#panel button');
  const panel = document.querySelector('#panel');
  const shuff = document.querySelector('#shuffle');
  const solve = document.querySelector('#solve');

  const sb = new SlidingBlocks(4, 4, 0.1);
  let mode = '';

  const swap = sb.step.bind(sb);

  const shuffle = () => {
    sb.shuffle();
    update();
  };

  const draw = (i) => {
    const n = sb.block[i] - 1;
    const x = n % 4 * tiles[i].offsetWidth;
    const y = (n / 4 | 0) * tiles[i].offsetHeight;
    tiles[i].style.backgroundImage = `url(${img.src})`;
    tiles[i].style.backgroundPosition = `-${x}px -${y}px`;
  };

  const update = () => {
    sb.block.forEach((x, i) => {
      tiles[i].textContent = x;
      tiles[i].style.visibility = x == 16 ? 'hidden' : 'visible';
      draw(i);
    });
  };

  const test = () => {
    if (sb.complete()) {
      const b = sb.get_blank();
      tiles[b].style.visibility = 'visible';
      tiles.forEach((x) => {
        x.classList.add('none');
      });
    }
  };

  const swappable = () => {
    const b = sb.get_blank();
    return tiles[b].style.visibility == 'hidden';
  }

  const moving = (elem, direction, duration) => {
    const opt = { easing: [ 'ease' ] };
    if (direction == -1) {
      opt.transform = [ 'translateX(-100%)' ];
    } else if (direction == -4) {
      opt.transform = [ 'translateY(-100%)' ];
    } else if (direction == 1) {
      opt.transform = [ 'translateX(100%)' ];
    } else if (direction == 4) {
      opt.transform = [ 'translateY(100%)' ];
    }
    elem.animate(opt, duration).onfinish = () => {
      update();
      test();
    };
  };

  const solver = async () => {
    return sb.solve();
  };

  panel.addEventListener('click', (e) => {
    if (mode == 'auto') return;
    if (e.target.tagName != 'BUTTON') return;
    if (!swappable()) return;
    const n = sb.block.indexOf(e.target.textContent | 0);
    const b = sb.get_blank();
    if (swap(n)) moving(tiles[n], b - n, 100);
  });

  shuff.addEventListener('click', () => {
    steps.textContent = '';
    tiles.forEach((x) => {
      x.classList.remove('none');
    });
    shuffle();
  });

  solve.addEventListener('click', () => {
    if (!swappable()) return;
    shuff.disabled = true;
    solve.disabled = true;
    mode = 'auto';
    solver().then((p) => {
      let start;
      const r = [];
      p.shift();
      for (const x of p) {
        r.unshift(x.block.indexOf(16));
      }
      steps.textContent = `${r.length} steps`;
      const step = (timestamp) => {
        if (start === undefined) start = timestamp;
        const elapsed = timestamp - start;
        if (elapsed > 150) {
          const n = r.pop(), b = sb.get_blank();
          if (swap(n)) moving(tiles[n], b - n, 100);
          start = timestamp;
        }
        if (r.length > 0) {
          window.requestAnimationFrame(step);
        } else {
          shuff.disabled = false;
          solve.disabled = false;
          mode = '';
        }
      };
      window.requestAnimationFrame(step);
    });
  });

  const img = new Image();
  img.src = 'images/game.png';
  img.onload = () => {
    const aspect = img.height / img.width;
    const resizeObserver = new ResizeObserver((entries, observe) => {
      for (const entry of entries) {
        if (entry.contentBoxSize) {
          const w = entry.contentBoxSize[0].inlineSize;
          const h = w * aspect;
          tiles.forEach((x, i) => {
            x.style.width = `${w / 4}px`;
            x.style.height = `${h / 4}px`;
            x.style.backgroundSize = `${w}px ${h}px`;
            draw(i);
          });
        }
      }
    });
    resizeObserver.observe(panel);
    shuffle();
  };

});

■ ソース(sliding_blocks.js)

sliding_blocks.js