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

JavaScript

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

完成イメージ

スタート画面

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

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

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

移動

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

完成!

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

作成

では実際に作成していこう。まずは任意の場所に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"></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 blankIdx; //現在の空のindexを保持

  const init = () => {
    //パネル要素をdiv要素として作成していく
    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.posId = i;
      panel.textContent = i + 1;
      if (i === size * size - 1) {
        panel.textContent = '';
        panel.classList.add('blank');
        blankIdx = i;
      }
      stage.append(panel); 
    }
    //stageの子要素リストをpanelsに代入
    panels = stage.children;
  };
  init();
});

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

ポイント解説

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

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;
}

今回,パネルは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 blankIdx; //現在の空のindexを保持
  stage.style.width = size * 80 + 'px';
  stage.style.gridTemplateColumns = `repeat(${size},1fr)`;

  const init = () => {
    //パネル要素をdiv要素として作成していく
    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.posId = i;
      panel.textContent = i + 1;
      if (i === size * size - 1) {
        panel.textContent = '';
        panel.classList.add('blank');
        blankIdx = i;
      }
      stage.append(panel); 
    }
    //stageの子要素リストをpanelsに代入
    panels = stage.children;
  };
  init();
});

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

参考

試しに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 blankIdx; //現在の空のindexを保持
  stage.style.width = size * 80 + 'px';
  stage.style.gridTemplateColumns = `repeat(${size},1fr)`;

  const init = () => {
    //パネル要素をdiv要素として作成していく
    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.posId = i;
      panel.textContent = i + 1;
      if (i === size * size - 1) {
        panel.textContent = '';
        panel.classList.add('blank');
        blankIdx = i;
      }
      stage.append(panel);
    }
    //stageの子要素リストをpanelsに代入
    panels = stage.children;
  };
  
  startBt.addEventListener('click', () => {
    /****シャッフル****/
    for (let i = 0; i < size ** 4; i++) {
      const dir = Math.floor(Math.random() * 4); //0上,1下,2右,3左
      //上下左右のpos
      const targetPositions = [
        blankIdx - size,
        blankIdx + size,
        blankIdx + 1,
        blankIdx - 1,
      ];
      //乱数の結果から今回の交換位置を求める
      let pos = targetPositions[dir];
      //交換位置が範囲外だったら何もしない
      if (pos < 0 || pos >= size * size) {
        continue;
      }
      //そのposがblankパネルに隣接していたら
      if (isAdjacentToblank(pos)) {
        //交換処理
        swap(pos);
      }
    }
  });
  /****引数に入ってきたパネル番号がblankパネルと上下左右に隣接しているかを返す関数 */
  const isAdjacentToblank = (pos) => {
    //上がブランク
    if (pos - size === blankIdx) return true;
    //下がブランク
    if (pos + size === blankIdx) return true;
    //右がブランク
    if ((pos + 1) % size !== 0 && pos + 1 === blankIdx) return true;
    //左がブランク
    if (pos % size !== 0 && pos - 1 === blankIdx) return true;

    return false;
  };
  /***blankパネルと引数に入ってきたパネル番号の表示を交換する関数 */
  const swap = (pos) => {
    //引数に入ってきたposからblankパネルと交換する番号の入ったDOMを取得
    const numPanel = panels[pos];
    //blankIdxを指定してblankパネルDOMを取得
    const blankPanel = panels[blankIdx];
    //blankパネルの表示をnumPanelの数字にする
    blankPanel.textContent = numPanel.textContent;
    //blankパネルではなくなるのでクラスblankを削除する。
    blankPanel.classList.remove('blank');
    //numパネルはblankになるのでtextContentを空文字にする
    numPanel.textContent = '';
    //classとしてblankを付与
    numPanel.classList.add('blank');
    //blankのパネルが変わったのでblankIdxを更新しておく
    blankIdx = pos;
  };
  init();
});

ポイント解説

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

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

ではゲームのメイン処理であるクリックしたパネルと空白を入れ替えていく処理を作成しよう。以下のように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 blankIdx; //現在の空のindexを保持
  stage.style.width = size * 80 + 'px';
  stage.style.gridTemplateColumns = `repeat(${size},1fr)`;

  const init = () => {
    //パネル要素をdiv要素として作成していく
    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.posId = i;
      panel.textContent = i + 1;
      if (i === size * size - 1) {
        panel.textContent = '';
        panel.classList.add('blank');
        blankIdx = i;
      }
      panel.onclick = click;
      stage.append(panel);
    }
    //stageの子要素リストをpanelsに代入
    panels = stage.children;
  };

  startBt.addEventListener('click', () => {
    /****シャッフル****/
    for (let i = 0; i < size ** 4; i++) {
      const dir = Math.floor(Math.random() * 4); //0上,1下,2右,3左
      //上下左右のpos
      const targetPositions = [
        blankIdx - size,
        blankIdx + size,
        blankIdx + 1,
        blankIdx - 1,
      ];
      //乱数の結果から今回の交換位置を求める
      let pos = targetPositions[dir];
      //交換位置が範囲外だったら何もしない
      if (pos < 0 || pos >= size * size) {
        continue;
      }
      //そのposがblankパネルに隣接していたら
      if (isAdjacentToblank(pos)) {
        //交換処理
        swap(pos);
      }
    }
  });
  /****引数に入ってきたパネル番号がblankパネルと上下左右に隣接しているかを返す関数 */
  const isAdjacentToblank = (pos) => {
    //上がブランク
    if (pos - size === blankIdx) return true;
    //下がブランク
    if (pos + size === blankIdx) return true;
    //右がブランク
    if ((pos + 1) % size !== 0 && pos + 1 === blankIdx) return true;
    //左がブランク
    if (pos % size !== 0 && pos - 1 === blankIdx) return true;

    return false;
  };
  /***blankパネルと引数に入ってきたパネル番号の表示を交換する関数 */
  const swap = (pos) => {
    //引数に入ってきたposからblankパネルと交換する番号の入ったDOMを取得
    const numPanel = panels[pos];
    //blankIdxを指定してblankパネルDOMを取得
    const blankPanel = panels[blankIdx];
    //blankパネルの表示をnumPanelの数字にする
    blankPanel.textContent = numPanel.textContent;
    //blankパネルではなくなるのでクラスblankを削除する。
    blankPanel.classList.remove('blank');
    //numパネルはblankになるのでtextContentを空文字にする
    numPanel.textContent = '';
    //classとしてblankを付与
    numPanel.classList.add('blank');
    //blankのパネルが変わったのでblankIdxを更新しておく
    blankIdx = pos;
  };
  const click = (e) => {
    //クリックされた要素のposIdを取得
    const pos = e.target.posId;
    //blankパネルと隣接しているかチェック
    if (isAdjacentToblank(pos)) {
      //隣接していたら交換
      swap(pos);
    }
  };
  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.ok{
  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.6;
}
#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 blankIdx; //現在の空のindexを保持
  stage.style.width = size * 80 + 'px';
  stage.style.gridTemplateColumns = `repeat(${size},1fr)`;

  const init = () => {
    //パネル要素をdiv要素として作成していく
    for (let i = 0; i < size * size; i++) {
      const panel = document.createElement('div');
      panel.posId = i;
      panel.textContent = i + 1;
      if (i === size * size - 1) {
        panel.textContent = '';
        panel.classList.add('blank');
        blankIdx = i;
      }
      panel.onclick = click;
      stage.append(panel); 
    }
    //stageの子要素リストをpanelsに代入
    panels = stage.children;
  };

  startBt.addEventListener('click', () => {
    msgBox.textContent = null;
    /****シャッフル****/
    for (let i = 0; i < size ** 4; i++) {
      const dir = Math.floor(Math.random() * 4); //0上,1下,2右,3左
      //上下左右のpos
      const targetPositions = [
        blankIdx - size,
        blankIdx + size,
        blankIdx + 1,
        blankIdx - 1,
      ];
      //乱数の結果から今回の交換位置を求める
      let pos = targetPositions[dir];
      //交換位置が範囲外だったら何もしない
      if (pos < 0 || pos >= size * size) {
        continue;
      }
      //そのposがblankパネルに隣接していたら
      if (isAdjacentToblank(pos)) {
        //交換処理
        swap(pos);
      }
    }
    check();
  });
  /****引数に入ってきたパネル番号がblankパネルと上下左右に隣接しているかを返す関数 */
  const isAdjacentToblank = (pos) => {
    //上がブランク
    if (pos - size === blankIdx) return true;
    //下がブランク
    if (pos + size === blankIdx) return true;
    //右がブランク
    if ((pos + 1) % size !== 0 && pos + 1 === blankIdx) return true;
    //左がブランク
    if (pos % size !== 0 && pos - 1 === blankIdx) return true;

    return false;
  };
  /***blankパネルと引数に入ってきたパネル番号の表示を交換する関数 */
  const swap = (pos) => {
    //引数に入ってきたposからblankパネルと交換する番号の入ったDOMを取得
    const numPanel = panels[pos];
    //blankIdxを指定してblankパネルDOMを取得
    const blankPanel = panels[blankIdx];
    //blankパネルの表示をnumPanelの数字にする
    blankPanel.textContent = numPanel.textContent;
    //blankパネルではなくなるのでクラスblankを削除する。
    blankPanel.classList.remove('blank');
    //numパネルはblankになるのでtextContentを空文字にする
    numPanel.textContent = '';
    //classとしてblankを付与
    numPanel.classList.add('blank');
    //blankのパネルが変わったのでblankIdxを更新しておく
    blankIdx = pos;
  };
  const click = (e) => {
    //クリックされた要素のposIdを取得
    const pos = e.target.posId;
    //blankパネルと隣接しているかチェック
    if (isAdjacentToblank(pos)) {
      //隣接していたら交換
      swap(pos);
      check();
    }
  };
  const check = () => {
    let okCount = 0;
    for (let panel of panels) {
      if (
        panel.posId !== blankIdx &&
        panel.posId === parseInt(panel.textContent) - 1
      ) {
        okCount++;
        panel.classList.add('ok');
      } else {
        panel.classList.remove('ok');
      }
    }
    if (okCount === size * size - 1) {
      msgBox.textContent = 'Complete!';
    }
  };
  init();
});

完成!

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

コメント

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