おなじみの15パズルを作成してみよう。
完成イメージ
スタート画面
完成図が表示されている。このように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>
<table id="table"></table>
<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 difficulty=500;//ゲームの複雑さ
const shuffleCount = size*difficulty;//シャッフルの回数
const table=document.getElementById("table");
const msgBox=document.getElementById("msgBox");
const startBt=document.getElementById("startBt");
let panels;//td要素を格納する配列
let emptyIdx;//現在の空のindexを保持
//初期化処理
const init=()=>{
panels=[];
table.textContent=null;
msgBox.textContent=null;
createStage();
}
//テーブルを作成
const createStage = ()=>{
for(let i=0;i<size;i++){
const tr=document.createElement("tr");
for(let j=0;j<size;j++){
const td=document.createElement("td");
td.posId=i*size + j;
td.textContent=td.posId+1;
if(td.posId === size*size-1){
td.textContent="";
td.classList.add("empty");
emptyIdx=td.posId;
}
panels.push(td);
tr.append(td);
}
table.append(tr);
}
};
init();
});
実行してみよう。以下のように表示されれば成功だ。

ポイント解説
●use strict
use strictを付与すると。厳格モードでのjs解釈となる。エラーが発見しやすくなるメリットがある。どう違うかは以下のリンクを参照。
“use strict”(厳格モード)を使うべきか?
main.cssの追加
cssフォルダにmain.cssを作成し、以下のように記述する
#table{
margin:0 auto;
background:#eee;
padding:10px;
}
td{
font-size:24px;
text-align:center;
width:60px;
height:60px;
line-height:60px;
border:2px solid #333;
border-radius:15px;
background:#ddfeff;
}
td.empty{
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;
}
以下のようになれば成功だ

シャッフルの処理を作成する。
今回のゲームではいつものようにシャッフルのアルゴリズムで配列をシャッフルするわけにはいかない。そうしてしまうと復元できないゲームになってしまう可能性が高い。なので空白の位置の周り4方向をランダムに抽選してそこと入れ替えていくという方針で行う。main.jsの以下の部分を追記する
'use strict';
document.addEventListener("DOMContentLoaded",()=>{
const size=4;//盤面の大きさ
const difficulty=500;//シャッフルの複雑さ
const shuffleCount=size*difficulty;
const table=document.getElementById("table");
const msgBox=document.getElementById("msgBox");
const startBt=document.getElementById("startBt");
let panels;//td要素を格納する配列
let emptyIdx;//現在の空のindexを保持
const init = ()=>{
panels=[];
table.textContent=null;
msgBox.textContent=null;
createStage();
};
const createStage = ()=>{
for(let i=0;i<size;i++){
const tr=document.createElement("tr");
for(let j=0;j<size;j++){
const td=document.createElement("td");
td.posId=i*size+j;
td.textContent=td.posId+1;
if(td.posId === size*size-1){
td.textContent="";
td.classList.add("empty");
emptyIdx=td.posId;
}
tr.append(td);
panels.push(td);
}
table.append(tr);
}
};
startBt.addEventListener("click",()=>{
for(let i=0;i<shuffleCount;i++){
const dir=Math.floor(Math.random()*4);//0上,1右,2下,3左
switch(dir){
case 0:
if(emptyIdx < size) continue;
swap(emptyIdx-size,emptyIdx);
emptyIdx-=size;
break;
case 1:
if((emptyIdx+1)% size === 0) continue;
swap(emptyIdx+1,emptyIdx);
emptyIdx++;
break;
case 2:
if(emptyIdx >= size*(size-1)) continue;
swap(emptyIdx+size,emptyIdx);
emptyIdx+=size;
break;
case 3:
if(emptyIdx % size === 0) continue;
swap(emptyIdx-1,emptyIdx);
emptyIdx--;
break;
}
}
});
const swap=(numPos,empPos)=>{
panels[empPos].textContent=panels[numPos].textContent;
panels[empPos].classList.remove("empty");
panels[numPos].textContent="";
panels[numPos].classList.add("empty");
};
init();
});
ポイント解説
shuffuleCountの回数分、シャッフルを試みる。シャッフルの方法は以下
1.どちらの方向と交換するか上下左右を抽選する
2.そちらの方向にパネルがなければ交換できないのでcontinue
3.そちらの方向にパネルがあればそこの数字と空白を入れ替える
4.空になった部分にemptyクラスをつけ、数字が入ったパネルからはemptyクラスを外す
5.empPosの更新
クリックして移動する処理を追加する
ではゲームのメイン処理であるクリックしたパネルと空白を入れ替えていく処理を作成しよう。以下のように追記する。
'use strict';
document.addEventListener("DOMContentLoaded",()=>{
const size=4;//盤面の大きさ
const difficulty=5;//シャッフルの複雑さ
const shuffleCount=size*difficulty
const table=document.getElementById("table");
const msgBox=document.getElementById("msgBox");
const startBt=document.getElementById("startBt");
let panels;//td要素を格納する配列
let emptyIdx;//現在の空のindexを保持
const init = ()=>{
panels=[];
table.textContent=null;
msgBox.textContent=null;
createStage();
};
const createStage = ()=>{
for(let i=0;i<size;i++){
const tr=document.createElement("tr");
for(let j=0;j<size;j++){
const td=document.createElement("td");
td.posId=i*size+j;
td.textContent=td.posId+1;
if(td.posId === size*size-1){
td.textContent="";
td.classList.add("empty");
emptyIdx=td.posId;
}
td.onclick=click;
tr.append(td);
panels.push(td);
}
table.append(tr);
}
};
startBt.addEventListener("click",()=>{
init();
for(let i=0;i<shuffleCount;i++){
const dir=Math.floor(Math.random()*4);//0上,1右,2下,3左
switch(dir){
case 0:
if(emptyIdx < size) continue;
swap(emptyIdx-size,emptyIdx);
emptyIdx-=size;
break;
case 1:
if((emptyIdx+1)% size === 0) continue;
swap(emptyIdx+1,emptyIdx);
emptyIdx++;
break;
case 2:
if(emptyIdx >= size*(size-1)) continue;
swap(emptyIdx+size,emptyIdx);
emptyIdx+=size;
break;
case 3:
if(emptyIdx % size === 0) continue;
swap(emptyIdx-1,emptyIdx);
emptyIdx--;
break;
}
}
});
const swap=(numPos,empPos)=>{
panels[empPos].textContent=panels[numPos].textContent;
panels[empPos].classList.remove("empty");
panels[numPos].textContent="";
panels[numPos].classList.add("empty");
};
const click=(e)=>{
const pos=e.target.posId;
if(pos >= size && panels[pos-size].textContent===""){
swap(pos,pos-size);
}else if((pos+1) % size !== 0 && panels[pos+1].textContent===""){
swap(pos,pos+1);
}else if(pos < size*(size-1) && panels[pos+size].textContent===""){
swap(pos,pos+size);
}else if(pos % size !== 0 && panels[pos-1].textContent===""){
swap(pos,pos-1);
}
};
init();
});
判定処理を作成する
正しく配置されているパネルの枠線を緑にする処理と、すべて揃ったときにCompleteを表示する処理を作成しよう。まずはmain.cssを以下のように追記する
#table{
margin:0 auto;
background:#eee;
padding:10px;
}
td{
font-size:24px;
text-align:center;
width:60px;
height:60px;
line-height:60px;
border:2px solid #333;
border-radius:15px;
background:#ddfeff;
}
td.empty{
background-color:#eee;
border-color:#eee;
}
td.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.8;
}
#msgBox{
width:200px;
margin:10px auto;
text-align:center;
font-size:20px;
height:30px;
line-height:30px;
}
続いてmain.jsに以下を追記
'use strict';
document.addEventListener("DOMContentLoaded",()=>{
const size=4;//盤面の大きさ
const difficulty=5;//シャッフルの複雑さ
const shuffleCount=size*difficulty
const table=document.getElementById("table");
const msgBox=document.getElementById("msgBox");
const startBt=document.getElementById("startBt");
let panels;//td要素を格納する配列
let emptyIdx;//現在の空のindexを保持
const init = ()=>{
panels=[];
table.textContent=null;
msgBox.textContent=null;
createStage();
};
const createStage = ()=>{
for(let i=0;i<size;i++){
const tr=document.createElement("tr");
for(let j=0;j<size;j++){
const td=document.createElement("td");
td.posId=i*size+j;
td.textContent=td.posId+1;
if(td.posId === size*size-1){
td.textContent="";
td.classList.add("empty");
emptyIdx=td.posId;
}
td.onclick=click;
tr.append(td);
panels.push(td);
}
table.append(tr);
}
};
startBt.addEventListener("click",()=>{
init();
for(let i=0;i<shuffleCount;i++){
const dir=Math.floor(Math.random()*4);//0上,1右,2下,3左
switch(dir){
case 0:
if(emptyIdx < size) continue;
swap(emptyIdx-size,emptyIdx);
emptyIdx-=size;
break;
case 1:
if((emptyIdx+1)% size === 0) continue;
swap(emptyIdx+1,emptyIdx);
emptyIdx++;
break;
case 2:
if(emptyIdx >= size*(size-1)) continue;
swap(emptyIdx+size,emptyIdx);
emptyIdx+=size;
break;
case 3:
if(emptyIdx % size === 0) continue;
swap(emptyIdx-1,emptyIdx);
emptyIdx--;
break;
}
}
check();
});
const swap=(numPos,empPos)=>{
panels[empPos].textContent=panels[numPos].textContent;
panels[empPos].classList.remove("empty");
panels[numPos].textContent="";
panels[numPos].classList.add("empty");
};
const click=(e)=>{
const pos=e.target.posId;
if(pos >= size && panels[pos-size].textContent===""){
swap(pos,pos-size);
}else if((pos+1) % size !== 0 && panels[pos+1].textContent===""){
swap(pos,pos+1);
}else if(pos < size*(size-1) && panels[pos+size].textContent===""){
swap(pos,pos+size);
}else if(pos % size !== 0 && panels[pos-1].textContent===""){
swap(pos,pos-1);
}
check();
};
const check=()=>{
let okCount=0;
for(let i=0;i<panels.length;i++){
if(panels[i].textContent==="") continue;
if(panels[i].posId === parseInt(panels[i].textContent)-1){
okCount++;
panels[i].classList.add("ok");
}else{
panels[i].classList.remove("ok");
}
}
if(okCount === size*size-1){
msgBox.textContent="Complete!";
}
};
init();
});
完成!
以上で完成だ。ベースとなる処理しか作成していないので色々カスタマイズしてみてほしい。
コメント