昔からある8パズル(エイトパズル)をC#で再現してみます。
この記事では、8パズルを作る上での考え方とコントロール配列の使い方についても一緒に学びます。
一応、この手のパズルは4×4の正方形を使った15パズルや、3×3の正方形を使った8パズルが代表例です。
スライディングパズルとも言われ、よくお土産物屋などで見かけます。
実行イメージを確認したい方はこちらからダウンロードしてインストールしてください。(Visual Studio2017で発行済みのファイル)
Wikipediaでは次のように説明されています。
15パズルは、スライディングブロックパズル(Sliding puzzle)のひとつである。 4×4のボードの上に4×4-1すなわち15枚の駒があり、1駒ぶんの空きを利用して駒をスライドし、駒を目的の配置にする。 3×3のボード上で8枚の駒で同様に遊ぶものは8パズルと呼ばれる。
では、早速作っていきましょう。
作成する8パズルの実行イメージ
起動時

クリア時

8パズルの画面を作る
Visual Studioを起動し
ファイル >新規作成 >プロジェクト… からWindowsフォームアプリケーションを選択してください。
プロジェクト名は9puzzleとしておきます。(わたしは最初9パズルだと勘違いしていました。8パズルでももちろんOKです)
今回は分かりやすさを第1に作成しようと考えたので、デザイン画面上にボタンを9つ配置します。
ボタンの名前はデフォルトのままで大丈夫です。
配置とイメージは次の通りです。

ボタンを左上から順に作成順に並べてください。
デザインは以上です。
ボタンをコントロール配列化する
デザイン画面上のボタンやラベルなどのコントロールは配列化することが出来ます。
今回のように同じような機能を持つコントロールを作成するときなど、コントロール配列は重宝するでしょう。
Form1.csの先頭部分で、コントロール配列用の変数を宣言しておきます。
using System;
using System.Windows.Forms;
namespace _9puzzle
{
public partial class Form1 : Form
{
// 9つのボタンを配列化するためのコントロール変数
Control[] cell = new Control[9];
:
:
Controlというクラスは、ボタンやラベルなどのコントロールオブジェクトを保存できるクラスです。
今回は、デザイン画面で作ったボタンを保存するために使います。
次にデザイン画面上の何もないところ(ボタンで一杯なのでスペースが少ないですが)ダブルクリックしてフォームロードイベントコードを表示させてください。
今回は、ボタンの数だけ配列を作ってデザイン画面で配置したbutton1~button9までのボタンをそれぞれcell[0]~cell[8]に保存しています。
// 起動時の処理
private void Form1_Load(object sender, EventArgs e)
{
this.Text = Application.ProductName;
// ボタンを配列化する
cell[0] = this.button1;
cell[1] = this.button2;
cell[2] = this.button3;
cell[3] = this.button4;
cell[4] = this.button5;
cell[5] = this.button6;
cell[6] = this.button7;
cell[7] = this.button8;
cell[8] = this.button9;
:
起動時の処理としてまずはボタンをコントロール配列化しておきます。
コントロール配列化することで、button1, button2 …を cell[0], cell[1]… と配列として扱えるようになります。
すなわちcell[0].Text とすればbutton1.Textを表すことになります。
これで9つのボタンがループ処理などで扱いやすくなりました。
つづけてフォームロードイベント内にボタンの文字とボタンを押したときのイベント設定を行います。
// ボタンの文字とイベント処理を設定
for(int i=0; i<cell.Length; i++)
{
// ボタンに数値を表示1~8まで(9は空欄にしておく)
cell[i].Text = (i + 1).ToString();
if (cell[i].Text == "9") cell[i].Text = "";
// ボタンのタグに番号をつける(0~8)
cell[i].Tag = i.ToString();
// ボタンクリック時のイベント処理を設定
cell[i].Click += new System.EventHandler(btnClick);
}
forループ中のcell.Lengthは要素数なので9が保存されています。
cell[i].Text = (i + 1).ToString(); if (cell[i].Text == "9") cell[i].Text = "";
でボタンのテキストを1~8に設定しています。(cell[0]には1が表示される)
if文は、最後のcell[8]を空欄にするためです。
Visual StudioのコントロールにはTagというプロパティがあります。これはコントロール名(=プロパティName)とは異なり、違うコントロール間で同じタグをつけて扱いを同じくしたり、コントロール名以外の値で何か判定したりするときに使える便利なプロパティです。
今回は、button1~button9に対して0~8までの値をTagとしてつけておきます。
cell[i].Tag = i.ToString();
今回はこのTagの値をコントロール配列の添え字に対応させておいて、配列の何番目添え字ののボタンをクリックしたか?を判定するために使います。
cell[0]ならcell[0].Tagには0が、cell[1]ならcell[1].Tagには1が保存されていることになります。
次ににボタンを押したときのイベント処理の設定です。
cell[i].Click += new System.EventHandler(btnClick);
System.EventHandlerの引数は、ボタンを押したときに実行するメソッド名です。
cell[n](nは0~8)をクリックしたら、btnClickメソッドを実行しなさい、という意味になります。
まだこの時点ではbtnClickメソッドは存在しません。後述します。
8パズルの場合、クリックした位置の前後左右のセルに空きがあれば移動できます。
btnClickメソッドではこの移動チェックと実際に移動できるときは数値の表示を入れ替えるという処理をすることになります。
フォームロードイベントの最後の処理として、先ほど設定した数値をバラバラに並べ替えることをします。
// ボタンのテキストをシャッフル
Random r = new Random();
for (int i=0; i<1000; i++)
{
// 入れ替え位置を乱数で設定
int no = r.Next(cell.Length);
// 入れ替え可能かチェック
checkSwap(no);
}
乱数でボタンの添え字番号0~8を発生させて、checkSwapメソッドに引数として渡しています。(checkSwapメソッドはこの後、作成します)
checkSwapメソッドでは入れ替え可能かの判定をして、可能であればボタンの数値文字の入れ替えを行います。
ランダムで入れ替え可能か?をチェックしているので1000回繰り返していますが、実際に入れ替えている回数をカウントしてみたところ300回前後でした。
このループ回数を減らせば難易度も下がることになります。
パネルが入れ替え可能かどうか?の判定
クリックしたパネルが入れ替え可能か?という判定が、たぶん、この8パズルの一番のポイントだと思います。
checkSwapメソッドで実現しています。
// 入れ替え可能かチェックする
private void checkSwap(int no)
{
if (no >= 3 && cell[no - 3].Text == "") // 上側と入れ替え
{
swapText(no, no - 3);
}
else if (no <= 5 && cell[no + 3].Text == "") // 下側と入れ替え
{
swapText(no, no + 3);
}
else if (no % 3 != 2 && cell[no + 1].Text == "") // 右側と入れ替え
{
swapText(no, no + 1);
}
else if (no % 3 != 0 && cell[no - 1].Text == "") // 左側と入れ替え
{
swapText(no, no - 1);
}
}
引数noが、チェックしたいボタンの番号(Tagまたはcellの添え字番号)です。
if文の処理にあるswapTextメソッドは数値文字の入れ替えを行うメソッドです。
// テキスト入れ替え
private void swapText(int n, int m)
{
string work = cell[n].Text;
cell[n].Text = cell[m].Text;
cell[m].Text = work;
}
引数に指定したn番目とm番目のコントロール配列の文字を入れ替えています。
変数aとbを入れ替える処理は、一時的に保存するための変数wを作っておいて
w = a; a = b; b = w;
が基本です。
ちなみにPythonでは、
a, b = b, a
で入れ替えが出来てしまいます。
引数にしてしたnoは、コントロール配列cellの添え字とそのプロパティTagに対応しています。
noが0ならbutton1を表す、ということになります。
ここで次のような3×3のパネルをイメージしてください。
| cell[0] | cell[1] | cell[2] |
| cell[3] | cell[4] | cell[5] |
| cell[6] | cell[7] | cell[8] |
今回はあえて2次元配列を使うほどでもないと思ったので1次元配列で実現しています。
物事はシンプルに考えた方がいいです。2次元的なデータでも今回のように1次元配列で十分間に合うことは多いと思います。
では、こんな風に考えてください。
| 添え字が3~8 | 上側のパネルとと入れ替えが可能 | no >= 3 |
| 添え字0~5 | 下側のパネルと入れ替えが可能 | no <= 5 |
| 添え字2、5、8以外 | 右側のパネルと入れ替えが可能 | n % 3 != 2 |
| 添え字0、3、6以外 | 左側のパネルと入れ替えが可能 | n % 3 != 0 |
これをプログラムに直したものが上記のcheckSwapメソッドです。
この部分は、他の言語に直してもあまり変わらないので応用がきくと思います。
パネルを選択した時の処理
パネルをユーザがクリックしたとき(今回は、ボタンを押したとき)に行う処理は次の流れです。
1.クリックしたパネル番号はどれか?
2.クリックしたパネルが入れ替え可能か?
可能: 入れ替える
不可: なにもしない
3.パネルが揃ったかどうかのチェック
揃った: ゲームクリア
ボタンイベントに設定したbtnClickメソッドです。
// ボタンクリック時の処理
private void btnClick(object sender, System.EventArgs e)
{
// 押されたボタンのTagを取得(0~8)
Button btn = (Button)sender;
// 入れ替え可能かチェック
checkSwap(no);
// 揃ったかチェック
checkText();
}
1.クリックしたパネル番号はどれか?
Object変数senderには、どのオブジェクトでそのイベントが発生したかが格納されています。
今回使っているコントロールはボタンですので、Buttonクラスにキャスト(型変換)しています。
Button btn = (Button)sender;
Tagプロパティは、そのままでは文字列扱いのためint型に直して数値として使えるようにします。
int no = int.Parse(btn.Tag.ToString());
2.入れ替え可能かのチェック
noにはクリックしたボタンの番号が保存されています。
checkSwapメソッドで、入れ替え可能かをチェックし可能ならパネルの文字列を入れ替えます。
3.パネルが揃ったかどうかのチェック
checkTextメソッドで行っています。
後述します。
パネルが揃ったかどうか?のチェック
パネルの左上から順に1~8まで揃ったかチェックします。
クリア時のイメージ
| 1 | 2 | 3 |
| 4 | 5 | 6 |
| 7 | 8 | 空欄 |
// 番号が揃ったかチェックする
private void checkText()
{
int i;
// 配列添え字0~7まで(最後の8は空欄になるため)
for(i=0; i<cell.Length-1; i++)
{
if(cell[i].Text != (i+1).ToString())
{
break;
}
}
// カウンタiが8なら全部揃っている
if(i == cell.Length-1)
{
MessageBox.Show("クリア!");
}
}
for文のループカウンタを使って、順番にチェックしていきます。
if(cell[i].Text != (i+1).ToString())
1つでも違っていればbreakでfor文を抜けるようにします。
1から8まで順にチェックできればカウンタ変数iの値は、8になっているはずです。
if(i == cell.Length-1)
すなわちこれがクリアした時の値です。
ソースコード全体(Form1.csに設定)
今回のプロジェクトのソースコード全体を示します。
9つのボタンのみ、デザイン画面で配置しておいてください。


コメント