塵芥回顧録

なるべく更新していきたいが、ネタがない。

15パズル(p5js)

15パズルをp5jsで作成しました。

 

目次

 

15パズルとは

下図のような4×4の正方形で作られたパズルで、空白のマスに隣接するパネルを移動することができます。番号がバラバラの状態から図1のように1~15まで順番に並べることができたらクリアとなります。今回は数字の代わりに画像を使用し、画像を完成させるとクリアにしました。(15パズルは基本画像を使うものだと思っていた。)

1 2 3 4
5 6 7 8
9 10 11 12
13 14 15  

図1.パズル(完成)

 

 

プログラムについて

コードはp5jsの右上にある</>を押すと見れます。

最初に書かれているnx=4で横のパネル数、ny=4で縦のパネル数を指定しています。数値を変えることでパネルの数を変更できますが、クリア不可能な問題が生成される可能性があります。

f:id:nupepon:20220323171146p:plain

プログラムの流れ

このプログラムは上図のような流れで動作します。

・preload
p5jsで画像を読み込むのに必要な関数です。画像のアップロードには画像を使用したいスケッチの左上にある"<"ボタンをクリックするとディレクトリが表示されるので、"スケッチファイル"の右にある"▼"をクリックし、"ファイルアップロード"を押すとできます。

・setup
取得した画像を350×350のサイズにリサイズし、キャンバスを「画像のサイズ+パネルの数-1」で作成しています。「パネルの数-1」はパネルの境目をわかりやすくするためのスペース確保で加えています。
for文では配列(cutimg)内に16枚にトリミングした画像を入れています。get(x, y, xsize, ysize)は座標(x,y)から右にxsizeピクセル、下にysizeピクセルの画像を取得します。
画像を代入したのちrandompuzzle(nx*ny)でパズルを生成します。引数はパネルの枚数になっています。

・randompuzzle
配列(pzstate)に0~15の値をランダムに配置する関数です。pzstateにはどのマスにどの番号のパネルがあるかを示しており、pzstate内に0~15の値が順番に並んでいると図1の状態を示します。(配列内の数値+1が図中の番号、16は空白を示す)
pzstateは16のマス内に0~15がそれぞれ一つ入っていなければならないので乱数を0~(15-配列への代入数)の範囲で生成し、図2のように最初のマスから生成した乱数分の位置数えた場所に順に値を配置しています。

f:id:nupepon:20220319204244p:plain

図2.ランダムイメージ図

生成した後、パズルがクリア可能か判定し、最初からクリアしている状態(図1のような状態)もしくはクリア不可能な場合は再度ランダムに配置し直します。
クリア可能か否かの判定は以下のサイトを参考にしています。

y-uti.hatenablog.jp

mathworld.wolfram.com

pzstate[i]==num-1は空白マスを示しており、pos += 1 + floor(i/nx)で空白マスの行番号を足しています。for(let j=i+1; j<num; j++)ではその配列番号(i)より右側にiに格納された数値より小さい値がいくつ含まれているかをカウントしています。これらを足し合わせた数が偶数であればそのパズルは解くことが可能らしいそうです(詳しくは分かりませんが)。判定での!=2ではなく!=ny%2としているのはnyを変えたときに対応できるようにny%2としています。前述のとおりこれで正しくクリア可能なパズルが生成できるかは分かりません。判定の中に含まれているclearcheck()ではそのパズルがクリアしている状態か否かを判定しています。クリア状態であればtrueを、未クリア状態であればfalseを返します。

・clearcheck
配列pzstateの中に0~15まで順番通りに配置されているか確認します。順番通りであればtrueを、そうでなければfalseを返します。

・drawboard
現在のパズルの状況を表示します。cutimg.length-1は空白のマスを示すためパネルを表示しません。ただしclearflag==true(クリア状態)の場合空白パネルも埋めるので空白のマスに15番目のパネルを表示します。cutimgに画像データが格納されており、パズルの状態はpzstateに格納されているため、左上から順に画像を表示する場合cutimg[pzstate[i]]となります。

・mousePressed
if(x >= nx || y >= ny) return 1;
枠外の場合反応しない。
if(checkempty(nx*ny, x, y)) movecount++;
checkempty関数で上下左右に空白のマスがあるか確認し、選択したパネルを空白のマスに移動する。また、手数(movecount)に1加える。
drawboard();
ボードを表示。
clearflag = clearcheck();
クリア状態の確認。
drawdate();
経過時間、手数を表示。

・checkempty(num, x, y)
numはパネルの数、x,yは選択したパネルの座標を示しています。
そのパネルが端か否か(確認するマスが存在するのか)を判定し、端でなければ隣接するマスに空白マスがあるかどうかを確認しています。
空白マスが存在する場合は選択したパネルと空白マスのpzstate値を交換します。

・drawdate
経過時間、手数を表示します。

実装した方がよさそうなもの

図1のようなパネル番号を画面上でも表示した方が良いかもしれません。
分かりやすい画像を使えばいいのかもしれませんが、位置が分かりにくいパネルも存在するかと思うので、パネルの端に番号を表示したり、番号と画像の切り替えができるようなシステムを実装した方が親切かと思いました。
最初からバラバラな状態で、読み込んだら即スタートもあまり良くない。最初は整列された状態でクリックしたらパネルをランダム配置し、カウントスタートが良いかと思います。少なくともカウントはクリックしてからの方が良いかと。