16パズルを作ってみる(後編)

こんにちは、きうちです。

今回は前回の「16パズルを作ってみる(前編)」の続きをお届けします。

★ ★ ★

前回までで、「ピースを並べる」「シャッフルする」「マウスクリックでピースを動かす」といったことはできるようになりました。

ただ、これだと綺麗に並べ終わっても何も起こらなくてさみしいですね。

「そろったか?」を判定して、そろっていたらメッセージが表示されるようにしてみましょうか。

そのためのメソッド、Judgeをフォームクラスに追加します。

        private bool Judge() // そろっているか判定する
        {
            bool done = true;

            for (int y = 0; y < cells.GetLength(0); y++)
            {
                for (int x = 0; x < cells.GetLength(1); x++)
                {
                    if (x == 3 && y == 3) // 一番右下については「0かどうか」を確認する
                    {
                        if (cells[x, y] != 0)
                        {
                            done = false;
                            break;
                        }
                    }
                    else if (cells[x, y] != y * 4 + x + 1) // それ以外は所定の値になっているか確認する
                    {
                        done = false;
                        break;
                    }
                }
            }

            return done;
        }

cellsを左上からずっと「所定の値になっているか?」を見ていきます。1つでもそうなっていない箇所があればその時点でdone(できたかどうかを示す変数)をfalseにしてbreak!

というわけで、戻り値として「できていればtrue、できていなければfalse」が返ります。

続いて、このメソッドを呼び出す箇所。

マウスクリックイベント処理に追加します。

        private void pictureBox1_MouseClick(object sender, MouseEventArgs e) // マウスクリック時処理
        {
            int x = e.X / 64;
            int y = e.Y / 64;
            MovePiece(x, y);
            if (Judge()) // 判定処理の呼び出しを追加
            {
                MessageBox.Show("Congratulations!"); // できていたらメッセージ表示
                SetupCells(); // メッセージダイアログのOKボタンを押したら、次のゲーム開始
                Shuffle();
                Refresh();
            }
        }

これで実行すると・・・ピースを所定の位置に並べ終わると「Congratulations!」と表示されます。

★ ★ ★

続いて、数字ではなくグラフィックにしてみましょうかね。

この前、都内某所で撮影した以下の風景写真にしてみたいと思います。

こんな風にプロジェクトに追加して、「出力ディレクトリにコピー」を「常にコピーする」にします。

そして、フィールドとしてBitmapの配列を16・・・本当は15でいいはずですが、なんとなく16。用意します。

また、画像を読み込んで64x64のピースにして上記配列に格納するメソッド PreparePiece を用意します。

        private Bitmap[] bmps = new Bitmap[16]; // ピース画像格納用

        private void PreparePiece() // ピース準備
        {
            Bitmap? srcBitmap = Image.FromFile("16puzzle_bg_sample.jpg") as Bitmap;
            srcBitmap?.SetResolution(96, 96);

            for (int x = 0; x < cells.GetLength(0); x++)
            {
                for (int y = 0; y < cells.GetLength(1); y++)
                {
                    Rectangle srcRect = new Rectangle(x * 64, y * 64, 63, 63);
                    bmps[y * 4 + x] = srcBitmap!.Clone(srcRect, srcBitmap.PixelFormat);
                }
            }
        }

コンストラクタにちょっと追加。

        public Form1() // コンストラクタ
        {
            InitializeComponent();

            PreparePiece(); // 追加

            SetupCells();
            Shuffle();
            Refresh();
        }

Paintイベントメソッドを次のように変更。

        private void pictureBox1_Paint(object sender, PaintEventArgs e) // ピクチャーボックス描画イベント
        {
            for (int x = 0; x < cells.GetLength(0); x++)
            {
                for (int y = 0; y < cells.GetLength(1); y++)
                {
                    if (cells[x, y] != 0)
                    {
                        //e.Graphics.FillRectangle(Brushes.AliceBlue, x * 64, y * 64, 63, 63);
                        //e.Graphics.DrawString(cells[x, y].ToString("D2"), new Font("MS ゴシック", 32), Brushes.Black, x * 64, y * 64 + 8);

                        e.Graphics.DrawImage(bmps[cells[x, y] - 1], new Point(x * 64, y * 64)); // 変更
                    }
                }
            }
        }

実行すると・・・おお!絵バージョンの16パズルになりました。ちょいムズいね!

★ ★ ★

最後に・・・動いているものを16パズルにしたいなと思います。題材としては簡単に「時計」とかでどうでしょ。

以下のように、時計を描くPaintイベントメソッドを用意します。(あとでpictureBox2という名前のピクチャーボックスを用意して、そこに紐づけます。)

        private void pictureBox2_Paint(object? sender, PaintEventArgs e) // 時計描画イベントメソッド
        {
            e.Graphics.DrawEllipse(Pens.White, 16, 16, 224, 224);

            var t = DateTime.Now;
            var dt = Math.PI / 30; // (=6.28/60)
            double theta;
            int cx = 128;
            int cy = 128;

            // 目盛りを描く
            for (int i = 0; i < 60; i += 5)
            {
                theta = i * dt - Math.PI / 2D;
                double x1 = Math.Cos(theta) * (i % 10 == 0 ? 95 : 100) + cx;
                double y1 = Math.Sin(theta) * (i % 10 == 0 ? 95 : 100) + cy;
                double x2 = Math.Cos(theta) * 110 + cx;
                double y2 = Math.Sin(theta) * 110 + cy;
                e.Graphics.DrawLine(Pens.White, (int)x1, (int)y1, (int)x2, (int)y2);
            }

            // 時針
            theta = (Math.PI / 6) * (12 - (t.Hour % 12)) + Math.PI / 2;
            double xx2 = Math.Cos(theta) * 60 + cx;
            double yy2 = -Math.Sin(theta) * 60 + cy;
            e.Graphics.DrawLine(Pens.Blue, cx, cy, (int)xx2, (int)yy2);

            // 分針
            theta = dt * (60 - t.Minute) + Math.PI / 2;
            xx2 = Math.Cos(theta) * 80 + cx;
            yy2 = -Math.Sin(theta) * 80 + cy;
            e.Graphics.DrawLine(Pens.Red, cx, cy, (int)xx2, (int)yy2);

            // 秒針
            theta = dt * (60 - t.Second) + Math.PI / 2;
            xx2 = Math.Cos(theta) * 100 + cx;
            yy2 = -Math.Sin(theta) * 100 + cy;
            e.Graphics.DrawLine(Pens.Green, cx, cy, (int)xx2, (int)yy2);
        }

細かい説明は省きます(それをすると本題からそれるので・・・)。

ちなみに、この描画イベントを普通に実行するとこんな感じになります。

続いて、次のようにフィールドとセットアップ処理を追加します。

        private PictureBox pictureBox2 = new(); // ピースのネタを描くためのピクチャーボックス

        private System.Windows.Forms.Timer timClock = new(); // 時計を再描画するためのタイマー

        private void SetupPictureBox2AndTimer()
        {
            // pictureBox2の準備
            pictureBox2.SetBounds(0, 0, 256, 256, BoundsSpecified.Size);
            pictureBox2.BackColor = Color.Black;
            pictureBox2.Paint += pictureBox2_Paint;
            pictureBox2.Refresh();

            // タイマーの準備
            timClock.Tick += timClock_Tick!;
            timClock.Interval = 1000;
            timClock.Start();

            // ピース作成
            PreparePiece2();
        }

        private void timClock_Tick(object sender, EventArgs e) // 時計更新タイマーTICKイベント処理
        {
            pictureBox2.Refresh(); // 時計を再描画する
            PreparePiece2(); // ピースを作る
            pictureBox1.Refresh(); // 16パズルを再描画する
        }

そして、コンストラクタを次のように変更します。

        public Form1() // コンストラクタ
        {
            InitializeComponent();

            //PreparePiece(); // 先ほど追加したものは消すかコメントアウト

            SetupPictureBox2AndTimer(); // 追加

            SetupCells();
            Shuffle();
            Refresh();
        }

実行すると・・・

ほら、時計が動いてる!

・・・これまた結構ムズい。

★ ★ ★

というわけで「16パズルを作ってみる(前編)」の続きをお届けしてきましたが、いかがでしたでしょうか。

工夫次第ではさらに面白いものができそうですね!

それではまた次回~(*´︶`*)ノ"またねー

★☆★☆★ 追伸 ★☆★☆★

前回と今回の分のソースを公開しました!
よかったらどうぞ~

https://github.com/YasuhikoKiuchi/Puzzle16