【ゲーム】JavaScript:37 15パズル

 「15パズル」は 4×4 の格子上に 1~15 の数字タイルと空白(0)があり、空白の周囲にあるタイルをスライドさせて数字を 1→2→…→15 の順に並べ替え、空白を最後尾に配置できればクリアとなる古典的なスライドパズルです。

遊び方・操作方法

  1. タイトル画面で「スタート ▶️」をクリック
  2. ゲーム画面に切り替わり、シャッフルされたタイルが表示されます。
  3. 空白(何も描かれていないマス)の上下左右にあるタイルをクリックすると空白と入れ替わります。
  4. すべてのタイルが 1→2→…→15 の順に並び、空白が右下に移動したらクリア!

ルール

  • マスは隣接するタイルのみスライド可能
  • シャッフルは必ず解ける(可解状態)のみ生成
  • 全て正しく揃えた時点でクリア画面へ

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

37 15パズル

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

15puzzle_title.png15puzzle_bg.png

ゲーム画面イメージ

プログラム全文(15puzzle.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>🧩 15パズル 🧩</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    /* ================= 全体リセット ================= */
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    body {
      background: url('15puzzle_bg.png') no-repeat center center fixed;
      background-size: cover;
      font-family: 'Arial', sans-serif;
      height: 100vh;
      user-select: none;
      position: relative;
      color: #333;
    }
    .hidden { display: none; }

    /* ================ オーバーレイ(タイトル/終了画面)共通 ================ */
    .overlay {
      position: absolute;
      top: 50%; left: 50%;
      transform: translate(-50%, -50%);
      background: rgba(255,255,255,0.95);
      padding: 20px;
      border-radius: 10px;
      width: 600px;          /* 横幅600px */
      max-width: 90%;
      box-shadow: 0 4px 8px rgba(0,0,0,0.3);
      text-align: center;
      z-index: 10;
    }

    /* ================ タイトル画面 ================ */
    #title-screen h1 {
      font-size: 2em;
      margin-bottom: 10px;
    }
    #title-screen .title-image {
      display: block;
      margin: 0 auto 10px;
      max-width: 80%;
      height: auto;
    }
    #title-screen .instructions-title {
      font-weight: bold;
      font-size: 1.1em;
      margin-top: 10px;
      text-align: center;
    }
    #title-screen .instructions {
      text-align: left;
      margin: 10px 0;
      line-height: 1.4;
      font-size: 0.95em;
    }
    #title-screen .btn {
      margin-top: 20px;
    }

    /* ================ ボタン共通 ================ */
    .btn {
      padding: 10px 20px;
      font-size: 1em;
      font-weight: bold;
      color: #fff;
      background-color: #007bff;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color .2s, transform .1s;
    }
    .btn:hover {
      background-color: #0056b3;
      transform: translateY(-2px);
    }
    .btn:active {
      transform: translateY(0);
    }

    /* ================ ゲーム画面コンテナ ================ */
    #game-screen {
      width: 600px;         /* 横幅600px */
      max-width: 90%;
      margin: 0 auto;
      padding-top: 40px;
    }
    #game-screen .game-container {
      background: rgba(255,255,255,0.85);
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 4px 8px rgba(0,0,0,0.2);
    }
    /* ステータスメッセージ */
    #status-message {
      margin-bottom: 10px;
      padding: 8px;
      background: rgba(0,0,0,0.7);
      color: #fff;
      border-radius: 5px;
      font-size: 1em;
    }

    /* 4×4 ボード */
    .board {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      grid-gap: 5px;
    }
    .tile {
      background: #ccc;      /* 色を濃く */
      border-radius: 5px;
      font-size: 1.5em;
      font-weight: bold;
      display: flex;
      align-items: center;
      justify-content: center;
      height: 80px;
      cursor: pointer;
      transition: background .2s;
    }
    .tile.empty {
      background: transparent;
      cursor: default;
    }
    .tile:hover:not(.empty) {
      background: #bbb;      /* ホバー時も濃く */
    }

    /* ================ 終了画面 ================ */
    #end-screen .btn {
      margin-top: 20px;
    }
  </style>
</head>
<body>
  <!-- タイトル画面 -->
  <div id="title-screen" class="overlay">
    <img src="15puzzle_title.png" alt="15パズル タイトル" class="title-image">
    <h1>🧩 15パズル 🧩</h1>
    <div class="instructions-title">📖 遊び方・ルール</div>
    <div class="instructions">
      <p>・4×4 のマスに 1〜15 の数字タイルが並んでいます。</p>
      <p>・1 箇所の空所を使って、隣接するタイルをスライドします。</p>
      <p>・タイルを並べ替え、1→2→…→15 になるよう揃えるとクリア!</p>
    </div>
    <button id="start-btn" class="btn">スタート ▶️</button>
  </div>

  <!-- ゲーム画面 -->
  <div id="game-screen" class="hidden">
    <div class="game-container">
      <!-- ステータスメッセージ -->
      <div id="status-message">数字をスライドして並べ替えてください</div>
      <!-- ボード -->
      <div id="board" class="board"></div>
    </div>
  </div>

  <!-- 終了画面 -->
  <div id="end-screen" class="overlay hidden">
    <h1>🎉 クリア! 🎉</h1>
    <p id="end-message" style="margin-top:10px;">おめでとうございます!15まで揃いました。</p>
    <button id="end-back-btn" class="btn">タイトル画面に戻る ↩️</button>
  </div>

  <script>
  // ================= 定数設定 =================
  const SIZE = 4;           // 4×4
  const TOTAL = SIZE * SIZE;
  let tiles = [];           // 盤面配列 0〜15, 0 は空所
  const boardEl = document.getElementById('board');
  const statusEl = document.getElementById('status-message');

  // DOM 要素
  const titleScreen = document.getElementById('title-screen');
  const gameScreen  = document.getElementById('game-screen');
  const endScreen   = document.getElementById('end-screen');
  const startBtn    = document.getElementById('start-btn');
  const endBackBtn  = document.getElementById('end-back-btn');

  /**
   * タイトル画面を表示
   */
  function showTitle() {
    titleScreen.classList.remove('hidden');
    gameScreen.classList.add('hidden');
    endScreen.classList.add('hidden');
  }

  /**
   * ゲーム画面を表示 & 初期化
   */
  function showGame() {
    titleScreen.classList.add('hidden');
    gameScreen.classList.remove('hidden');
    endScreen.classList.add('hidden');
    initTiles();
    shuffleTiles();
    renderBoard();
    statusEl.textContent = '数字をスライドして並べ替えてください';
  }

  /**
   * 終了画面を表示
   */
  function showEnd() {
    titleScreen.classList.add('hidden');
    gameScreen.classList.add('hidden');
    endScreen.classList.remove('hidden');
  }

  /**
   * 盤面配列を初期配置 [1..15,0]
   */
  function initTiles() {
    tiles = [];
    for (let i = 1; i < TOTAL; i++) tiles.push(i);
    tiles.push(0);
  }

  /**
   * 配列をシャッフルして可解状態にする
   */
  function shuffleTiles() {
    do {
      for (let i = tiles.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [tiles[i], tiles[j]] = [tiles[j], tiles[i]];
      }
    } while (!isSolvable() || isSolved());
  }

  /**
   * 可解判定 (4幅盤)
   */
  function isSolvable() {
    let inv = 0;
    for (let i = 0; i < tiles.length; i++) {
      for (let j = i + 1; j < tiles.length; j++) {
        if (tiles[i] && tiles[j] && tiles[i] > tiles[j]) inv++;
      }
    }
    const blankRow = Math.floor(tiles.indexOf(0) / SIZE) + 1;
    return ((inv + blankRow) % 2) === 0;
  }

  /**
   * すべて揃っているか判定
   */
  function isSolved() {
    for (let i = 0; i < TOTAL - 1; i++) {
      if (tiles[i] !== i + 1) return false;
    }
    return true;
  }

  /**
   * ボードを描画
   */
  function renderBoard() {
    boardEl.innerHTML = '';
    tiles.forEach((value, idx) => {
      const tile = document.createElement('div');
      tile.classList.add('tile');
      if (value === 0) {
        tile.classList.add('empty');
      } else {
        tile.textContent = value;
        tile.addEventListener('click', () => moveTile(idx));
      }
      boardEl.appendChild(tile);
    });
  }

  /**
   * idx のタイルを空所と交換できれば交換
   */
  function moveTile(idx) {
    const zeroIdx = tiles.indexOf(0);
    const zx = zeroIdx % SIZE, zy = Math.floor(zeroIdx / SIZE);
    const x = idx % SIZE, y = Math.floor(idx / SIZE);
    if (Math.abs(zx - x) + Math.abs(zy - y) === 1) {
      [tiles[zeroIdx], tiles[idx]] = [tiles[idx], tiles[zeroIdx]];
      renderBoard();
      if (isSolved()) {
        statusEl.textContent = '🎉 クリア! 🎉';
        setTimeout(showEnd, 500);
      }
    }
  }

  // イベント登録
  startBtn.addEventListener('click', showGame);
  endBackBtn.addEventListener('click', showTitle);

  // 初期表示
  document.addEventListener('DOMContentLoaded', showTitle);
  </script>
</body>
</html>

アルゴリズムの流れ

ステップ処理
1showTitle() でタイトル表示
2「スタート」で showGame() → 初期化
3initTiles():1~15,0 をセット
4shuffleTiles():解けるまでシャッフル
5renderBoard():タイル要素を描画
6クリックで moveTile(idx) 呼出
7移動可能ならスライド → isSolved()
8解いたら少し待って showEnd()

関数の詳細解説

関数名役割
showTitle()タイトル/ゲーム/終了画面の表示切り替え
showGame()ゲーム開始時の初期化とシャッフル&描画
initTiles()配列を [1,2,…,15,0] に初期化
shuffleTiles()可解性チェック付きシャッフル
isSolvable()逆転数+空行位置の和で可解性を判定
isSolved()タイルが昇順に並んでいるか判定
renderBoard()現在の配列から DOM にタイル要素を動的に生成
moveTile(idx)クリックしたタイルを隣接空白と入れ替え、クリア判定

改造のポイント

  • 盤面サイズ変更const SIZE=4 を 3 〜 5 などに変えれば n-puzzle に拡張可能。
  • タイル画像:数字ではなく背景に任意の画像を当てはめて「絵合わせパズル」に。
  • タイム&手数計測Date.now() やカウントでクリアタイムや手数を表示するとやり込み要素UP。
  • 解答支援:オートソルバー(A* アルゴリズムなど)を組み込んで「ヒント」機能を追加。
  • アニメーション:クリック時のスライドを CSS トランジションでアニメ化すると見映え良くなります。

アドバイス
ぜひこの記事をもとに、自分だけのオリジナル要素をプラスして15パズルを進化させてみてください!