JSによるゲーム制作-(PanelPazzle 2024版)

JavaScript

おなじみの15パズルを作成してみよう。以前の記事ではtableタグを使って作成していたが、2024版ではgridレイアウトを使って並べている。

完成イメージ

スタート画面

完成図が表示されている。このように1~15まで連続で並べられれば完成だ。

スタートボタンでスタート

スタートボタンを押すとシャッフルされる。正しい場所にあるパネルは緑の枠線が表示される。

移動

15パズルの要領でパネルをクリックして入れ替えいく。動かせるパネルは空白と上下左右で隣接しているパネルだけだ。

コンプリート!

すべてのパネルが元通りになれば完成だ。Complete!の文字が表示される。
スタートを押すと再びシャッフルされる。

作成

16枚のパネル用のdiv要素作成

では実際に作成していこう。まずは任意の場所にpanelpazzleフォルダを作成し、 その中にcssフォルダ、jsフォルダを作成する。

panelpazzleフォルダの直下にindex.htmlを以下のように作成する。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>パネルパズル</title>
  <link rel="stylesheet" href="css/main.css">
</head>
<body>
  <div id="stage"></div>
  <p id="msgBox"></p>
  <button id="startBt">START</button>
  <script src="js/main.js" defer></script>
</body>
</html>

main.jsの作成

jsフォルダの中にmain.jsを作成し、以下のように記述する

'use strict';
document.addEventListener('DOMContentLoaded', () => {
  const size = 4; // 盤面の大きさ
  const stage = document.getElementById('stage');
  const msgBox = document.getElementById('msgBox');
  const startBt = document.getElementById('startBt');
  let panels = []; // stageの子要素(divリスト)
  let blankIndex; // 現在の空白パネルのインデックス


  // 初期化関数
  const init = () => {
    panels = []; // 前回のパネルリストをクリア
    stage.innerHTML = ''; // 前回のステージ内容をクリア

    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.dataset.index = i; // パネルのインデックスをdata属性に設定
      panel.textContent = i + 1;

      if (i === size * size - 1) {
        panel.textContent = ''; // 空白パネル
        panel.classList.add('blank');
        blankIndex = i;
      }

      stage.append(panel);
      panels.push(panel);
    }
  };

  init();
});

実行してみよう。検証から開発者ツールでも開いてみて以下のようにstageというidがついたdivのなかに子要素として16個のdiv要素が作成されていれば成功だ。
data-indexにindex番号が付与され、最後の要素にはclassとしてblankが付与されている。

ポイント解説

●use strict
use strictを付与すると。厳格モードでのjs解釈となる。エラーが発見しやすくなるメリットがある。どう違うかは以下のリンクを参照。
“use strict”(厳格モード)を使うべきか?

●panels配列
stage.children(DOMCollection)を用いて今後の処理を行ってくこともできるが、DOMCollectionをそのまま扱うとパフォーマンスが落ちることや可読性も悪いことから、ここではpanels配列を用意してDOMを格納しておく。

パネルのスタイリング

div要素にスタイルを当ててパネルにしていこう。
今回,パネルはgridレイアウトで並べていく、ステージの大きさは列数に比例して大きくしていきたいので、幅の指定などは以下のようにjsに追記する。

'use strict';
document.addEventListener('DOMContentLoaded', () => {
  const size = 4; // 盤面の大きさ
  const stage = document.getElementById('stage');
  const msgBox = document.getElementById('msgBox');
  const startBt = document.getElementById('startBt');
  let panels = []; // stageの子要素(divリスト)
  let blankIndex; // 現在の空白パネルのインデックス
  
  // ステージの設定
  stage.style.width = `${size * 80}px`;
  stage.style.gridTemplateColumns = `repeat(${size}, 1fr)`;

  // 初期化関数
  const init = () => {
    panels = []; // 前回のパネルリストをクリア
    stage.innerHTML = ''; // 前回のステージ内容をクリア

    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.dataset.index = i; // パネルのインデックスをdata属性に設定
      panel.textContent = i + 1;

      if (i === size * size - 1) {
        panel.textContent = ''; // 空白パネル
        panel.classList.add('blank');
        blankIndex = i;
      }

      stage.append(panel);
      panels.push(panel);
    }
  };

  init();
});

main.cssの追加

cssフォルダにmain.cssを作成し、以下のように記述する

#stage {
  aspect-ratio: 1 / 1;
  margin: 0 auto;
  background: #eee;
  padding: 10px;
  display: grid;
  gap: 4px;
}

#stage div {
  aspect-ratio: 1 / 1;
  font-size: 24px;
  border: 2px solid #333;
  border-radius: 15px;
  background: #ddfeff;
  display: flex;
  justify-content: center;
  align-items: center;
}

#stage div.blank {
  background-color: #eee;
  border-color: #eee;
}

#startBt {
  display: block;
  width: 200px;
  margin: 0 auto;
  height: 50px;
  box-shadow: 0 3px 0 5px #777;
}

#startBt:hover {
  cursor: pointer;
  opacity: 0.8;
}

以下のようになれば成功だ

参考

試しにjs以下の部分を6に設定してみよう。ステージが大きさに比例して拡大することがわかる。

'use strict';
document.addEventListener('DOMContentLoaded', () => {
  const size = 6; //盤面の大きさ
  const stage = document.getElementById('stage');
  const msgBox = document.getElementById('msgBox');
  const startBt = document.getElementById('startBt');

シャッフルの処理を作成する。

今回のゲームではいつものようにシャッフルのアルゴリズムで配列をシャッフルするわけにはいかない。そうしてしまうと正しい位置にパネルを復元できないゲームになってしまう可能性が高い。なので空白の位置の周り4方向をランダムに抽選してそこと入れ替えていくという方針で行う。main.jsの以下の部分を追記する。(最後にinit()が来るようにすること)

'use strict';
document.addEventListener('DOMContentLoaded', () => {
  const size = 4; // 盤面の大きさ
  const stage = document.getElementById('stage');
  const msgBox = document.getElementById('msgBox');
  const startBt = document.getElementById('startBt');
  let panels = []; // stageの子要素(divリスト)
  let blankIndex; // 現在の空白パネルのインデックス
  const shuffleIterations = size ** 4; // シャッフル回数
  
  // ステージの設定
  stage.style.width = `${size * 80}px`;
  stage.style.gridTemplateColumns = `repeat(${size}, 1fr)`;

  // 初期化関数
  const init = () => {
    panels = []; // 前回のパネルリストをクリア
    stage.innerHTML = ''; // 前回のステージ内容をクリア

    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.dataset.index = i; // パネルのインデックスをdata属性に設定
      panel.textContent = i + 1;

      if (i === size * size - 1) {
        panel.textContent = ''; // 空白パネル
        panel.classList.add('blank');
        blankIndex = i;
      }

      stage.append(panel);
      panels.push(panel);
    }
  };
  // パネルが空白と隣接しているかチェック
  const isAdjacentToBlank = (index) => {
    return (
      index - size === blankIndex || // 上で空白と隣接
      index + size === blankIndex || // 下
      (index + 1) % size !== 0 && index + 1 === blankIndex || // 右
      index % size !== 0 && index - 1 === blankIndex // 左
    );
  };

  // 空白パネルとクリックされたパネルを交換
  const swapPanels = (index) => {
    const targetPanel = panels[index];
    const blankPanel = panels[blankIndex];

    blankPanel.textContent = targetPanel.textContent;
    blankPanel.classList.remove('blank');

    targetPanel.textContent = '';
    targetPanel.classList.add('blank');

    blankIndex = index;
  };

  // シャッフル処理
  const shufflePanels = () => {
    for (let i = 0; i < shuffleIterations; i++) {
      const direction = Math.floor(Math.random() * 4); // 0: 上, 1: 下, 2: 右, 3: 左
      const targetPositions = [
        blankIndex - size,
        blankIndex + size,
        blankIndex + 1,
        blankIndex - 1
      ];

      const targetIndex = targetPositions[direction];
      if(targetIndex < 0 || targetIndex >= size * size) continue;
      if (isAdjacentToBlank(targetIndex)) {
        swapPanels(targetIndex);
      }
    }
  };
  // スタートボタンのクリック処理
  startBt.addEventListener('click', () => {
    msgBox.textContent = '';
    shufflePanels();
  });

  init();
});

ポイント解説

shuffuleCountの回数分、シャッフルを試みる。シャッフルの方法は以下
1.現在のブランク位置からどの方向と交換するか上下左右を抽選する
2.抽選結果が番外だったら何もしないでcontinue
3.抽選した場所が、ブランクと交換可能なのかをチェック
4.交換可能であればテキストを交換し、blankクラスの調整も合わせて行う。
5.blankIndexの更新

クリックして移動する処理を追加する

ではゲームのメイン処理であるクリックしたパネルと空白を入れ替えていく処理を作成しよう。以下のように2箇所、追記する。

'use strict';
document.addEventListener('DOMContentLoaded', () => {
  const size = 4; // 盤面の大きさ
  const stage = document.getElementById('stage');
  const msgBox = document.getElementById('msgBox');
  const startBt = document.getElementById('startBt');
  let panels = []; // stageの子要素(divリスト)
  let blankIndex; // 現在の空白パネルのインデックス
  const shuffleIterations = size ** 4; // シャッフル回数
  
  // ステージの設定
  stage.style.width = `${size * 80}px`;
  stage.style.gridTemplateColumns = `repeat(${size}, 1fr)`;

  // 初期化関数
  const init = () => {
    panels = []; // 前回のパネルリストをクリア
    stage.innerHTML = ''; // 前回のステージ内容をクリア

    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.dataset.index = i; // パネルのインデックスをdata属性に設定
      panel.textContent = i + 1;

      if (i === size * size - 1) {
        panel.textContent = ''; // 空白パネル
        panel.classList.add('blank');
        blankIndex = i;
      }
      panel.addEventListener('click', handlePanelClick);
      stage.append(panel);
      panels.push(panel);
    }
  };
  // パネルクリック時の処理
  const handlePanelClick = (event) => {
    const clickedIndex = parseInt(event.target.dataset.index);
    if (isAdjacentToBlank(clickedIndex)) {
      swapPanels(clickedIndex);
    }
  };
  // パネルが空白と隣接しているかチェック
  const isAdjacentToBlank = (index) => {
    return (
      index - size === blankIndex || // 上
      index + size === blankIndex || // 下
      (index + 1) % size !== 0 && index + 1 === blankIndex || // 右
      index % size !== 0 && index - 1 === blankIndex // 左
    );
  };

  // 空白パネルとクリックされたパネルを交換
  const swapPanels = (index) => {
    const targetPanel = panels[index];
    const blankPanel = panels[blankIndex];

    blankPanel.textContent = targetPanel.textContent;
    blankPanel.classList.remove('blank');

    targetPanel.textContent = '';
    targetPanel.classList.add('blank');

    blankIndex = index;
  };

  // シャッフル処理
  const shufflePanels = () => {
    for (let i = 0; i < shuffleIterations; i++) {
      const direction = Math.floor(Math.random() * 4); // 0: 上, 1: 下, 2: 右, 3: 左
      const targetPositions = [
        blankIndex - size,
        blankIndex + size,
        blankIndex + 1,
        blankIndex - 1
      ];

      const targetIndex = targetPositions[direction];
      if(targetIndex < 0 || targetIndex >= size * size) continue;
      if (isAdjacentToBlank(targetIndex)) {
        swapPanels(targetIndex);
      }
    }
  };
  // スタートボタンのクリック処理
  startBt.addEventListener('click', () => {
    msgBox.textContent = '';
    shufflePanels();
  });

  init();
});

ブランクパネルに隣接した、パネルをクリックしてみよう。ブランクパネルが入れ替われば成功だ。交換できないパネルをクリックしてもなにも起こらないことも確認すること。

判定処理を作成する

正しく配置されているパネルの枠線を緑にする処理と、すべて揃ったときにCompleteを表示する処理を作成しよう。まずはmain.cssを以下のように2箇所、追記する

#stage {
  aspect-ratio: 1 / 1;
  margin: 0 auto;
  background: #eee;
  padding: 10px;
  display: grid;
  gap: 4px;
}

#stage div {
  aspect-ratio: 1 / 1;
  font-size: 24px;
  border: 2px solid #333;
  border-radius: 15px;
  background: #ddfeff;
  display: flex;
  justify-content: center;
  align-items: center;
}

#stage div.blank {
  background-color: #eee;
  border-color: #eee;
}

#stage div.correct{
  border-color:lightgreen;
}

#startBt {
  display: block;
  width: 200px;
  margin: 0 auto;
  height: 50px;
  box-shadow: 0 3px 0 5px #777;
}

#startBt:hover {
  cursor: pointer;
  opacity: 0.8;
}

#msgBox{
  width:200px;
  margin:10px auto;
  font-size:20px;
  display:flex;
  justify-content: center;
  align-items:center;
}

続いてmain.jsに以下を追記

'use strict';
document.addEventListener('DOMContentLoaded', () => {
  const size = 4; // 盤面の大きさ
  const stage = document.getElementById('stage');
  const msgBox = document.getElementById('msgBox');
  const startBt = document.getElementById('startBt');
  let panels = []; // stageの子要素(divリスト)
  let blankIndex; // 現在の空白パネルのインデックス
  const shuffleIterations = size ** 4; // シャッフル回数
  
  // ステージの設定
  stage.style.width = `${size * 80}px`;
  stage.style.gridTemplateColumns = `repeat(${size}, 1fr)`;

  // 初期化関数
  const init = () => {
    panels = []; // 前回のパネルリストをクリア
    stage.innerHTML = ''; // 前回のステージ内容をクリア

    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.dataset.index = i; // パネルのインデックスをdata属性に設定
      panel.textContent = i + 1;

      if (i === size * size - 1) {
        panel.textContent = ''; // 空白パネル
        panel.classList.add('blank');
        blankIndex = i;
      }
      panel.addEventListener('click', handlePanelClick);
      stage.append(panel);
      panels.push(panel);
    }
  };
  // パネルクリック時の処理
  const handlePanelClick = (event) => {
    const clickedIndex = parseInt(event.target.dataset.index);
    if (isAdjacentToBlank(clickedIndex)) {
      swapPanels(clickedIndex);
      checkCompletion();
    }
  };
  // パネルが空白と隣接しているかチェック
  const isAdjacentToBlank = (index) => {
    return (
      index - size === blankIndex || // 上
      index + size === blankIndex || // 下
      (index + 1) % size !== 0 && index + 1 === blankIndex || // 右
      index % size !== 0 && index - 1 === blankIndex // 左
    );
  };

  // 空白パネルとクリックされたパネルを交換
  const swapPanels = (index) => {
    const targetPanel = panels[index];
    const blankPanel = panels[blankIndex];

    blankPanel.textContent = targetPanel.textContent;
    blankPanel.classList.remove('blank');

    targetPanel.textContent = '';
    targetPanel.classList.add('blank');

    blankIndex = index;
  };

  // シャッフル処理
  const shufflePanels = () => {
    for (let i = 0; i < shuffleIterations; i++) {
      const direction = Math.floor(Math.random() * 4); // 0: 上, 1: 下, 2: 右, 3: 左
      const targetPositions = [
        blankIndex - size,
        blankIndex + size,
        blankIndex + 1,
        blankIndex - 1
      ];

      const targetIndex = targetPositions[direction];
      if(targetIndex < 0 || targetIndex >= size * size) continue;
      if (isAdjacentToBlank(targetIndex)) {
        swapPanels(targetIndex);
      }
    }
    checkCompletion();
  };
  // スタートボタンのクリック処理
  startBt.addEventListener('click', () => {
    msgBox.textContent = '';
    shufflePanels();
  });
  // 完成判定
  const checkCompletion = () => {
    let completedCount = 0;

    panels.forEach((panel, idx) => {
      const isCorrect =
        idx !== blankIndex && idx === parseInt(panel.textContent) - 1;

      if (isCorrect) {
        completedCount++;
        panel.classList.add('correct');
      } else {
        panel.classList.remove('correct');
      }
    });

    if (completedCount === size * size - 1) {
      msgBox.textContent = 'Complete!';
    }
  };

  init();
});

完成!

以上で完成だ。ベースとなる処理しか作成していないので色々カスタマイズしてみてほしい。

関連記事

コメント

タイトルとURLをコピーしました