一筆書きのパズル(前編)

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

8月も終わりになってくると、「夏が終わっちゃうな…」とさみしい気持ちにもなりますね。

しかし、暑い日はまだまだ続きそうですね。

引き続き、暑さに負けないように頑張っていきましょう!

★ ★ ★

今回は「一筆書き」をモチーフにしたパズルゲームを作ってみたいと思います。

これも別に私のオリジナルというわけではなくて、私が昔そんなのを見かけたのを

思い出しながら作ってみる、という感じのものです。

画面に表示される「主人公」をキーボードの矢印キーで上下左右に操作していくと

その軌跡ができ、画面上の隙間をその軌跡で埋め尽くすとクリア、というもの。

ただし、一度通った場所は二度は通れない、という仕組み。

言語は例によってC#にします。Windowsフォームアプリとして作成します。

20×20のセルを1セルあたり32ピクセルで描くことにします。

このあたりは定数定義して使用することにします

画面サイズは 660×686にします。

セルの内容は cells というフィールドで持つことにします。

主人公(以下「自機」と記載)の位置を保持するため、mx, myというフィールド変数も用意します。

    public partial class Form1 : Form
    {
        private const int width = 20; // 横のセル数

        private const int height = 20; // 縦のセル数

        private const int size = 32; // 1セルあたりのサイズ(縦・横のピクセル数)

        private int[,] cells = new int[width, height]; // セル

        private int mx = width / 2; // 自機のX座標

        private int my = height / 2; // 自機のY座標


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

            Init();

            Refresh();
        }

        private void Init() // セル初期化
        {
            // 0で埋め尽くす
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    cells[x, y] = 0;
                }
            }

            cells[mx, my] = 1; // 最初に自機がいる箇所に軌跡を置く
        }
    }
}

描画メソッドはこんな風にします。

        private void Form1_Paint(object sender, PaintEventArgs e) // 描画メソッド
        {
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    if (mx == x && my == y) // 自機の座標だったらピンク色の四角を描く
                    {
                        e.Graphics.FillRectangle(Brushes.Salmon, mx * size, my * size, size, size);
                    }
                    else if (cells[x, y] == 0) // 何もないセルだったら青い四角(塗りつぶしなし)を描く
                    {
                        e.Graphics.DrawRectangle(Pens.DeepSkyBlue, x * size, y * size, size, size);
                    }
                    else if (cells[x, y] == 1) // 軌跡のセルだったら青い四角(塗りつぶし)を描く
                    {
                        e.Graphics.FillRectangle(Brushes.DeepSkyBlue, x * size, y * size, size, size);
                    }
                }
            }
        }

実行すると・・・

何もない世界があって、その中央に自機(ピンクの四角)がいますね!

★ ★ ★

移動処理を入れてみましょう。

フォームのKeyDownイベントを設定します。

そして、矢印キーの押されたものによってそれぞれ処理していきます。

画面からはみ出すような移動はさせないために、例えば↑を押した時、移動すると座標がマイナスになってしまうようであれば移動しないようにします。

また、軌跡となったところはもう移動できないルールですから、移動先をチェックして、何もない箇所でなければ移動しないようにします。

        private void Form1_KeyDown(object sender, KeyEventArgs e) // キー押下時処理
        {
            try
            {
                bool move = false; // 動きがあったか
                int mx0 = mx; // 現在の自機のX座標を保持
                int my0 = my; // 現在の自機のY座標を保持

                switch (e.KeyCode)
                    {
                    case Keys.Up: // ↑押下時
                        my--;
                        if (my == -1 || cells[mx, my] != 0) my = my0;
                        else move = true;
                        break;
                    case Keys.Down: // ↓押下時
                        my++;
                        if (my == height || cells[mx, my] != 0) my = my0;
                        else move = true;
                        break;
                    case Keys.Left: // ←押下時
                        mx--;
                        if (mx == -1 || cells[mx, my] != 0) mx = mx0;
                        else move = true;
                        break;
                    case Keys.Right: // →押下時
                        mx++;
                        if (mx == width || cells[mx, my] != 0) mx = mx0;
                        else move = true;
                        break;
                }

                if (move)
                {
                    cells[mx, my] = 1; // 軌跡を置く
                    Refresh(); // 画面再描画
                    e.Handled = true; // キー押下時の他の処理が動作しないようにする(なくても問題なさそうだけど一応)
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }

実行すると・・・

おおお!自機が矢印キーを押した方向に動いて、軌跡ができましたね!

★ ★ ★

では、パズルとして成立させるために…

  1. 一筆書きで埋めるための空間づくりをする
  2. 埋め尽くしたかどうか判定し、OKならおめでとうメッセージを表示する

というのを追加します。

1. について、Initメソッドを次のように変更します。

        private void Init() // セル初期化
        {
            // 2で埋め尽くす
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    cells[x, y] = 2;
                }
            }

            // 中央あたりに四角く空間を空ける
            for (int x = 5; x < 15; x++)
            {
                for (int y = 5; y < 15; y++)
                {
                    cells[x, y] = 0;
                }
            }

            // 最初に自機がいる箇所に軌跡を置く
            cells[mx, my] = 1;
        }

また、描画処理についてセルの値が2の場合の処理も追加します。

        private void Form1_Paint(object sender, PaintEventArgs e) // 描画メソッド
        {
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    if (mx == x && my == y) // 自機の座標だったらピンク色の四角を描く
                    {
                        e.Graphics.FillRectangle(Brushes.Salmon, mx * size, my * size, size, size);
                    }
                    else if (cells[x, y] == 0) // 何もないセルだったら青い四角(塗りつぶしなし)を描く
                    {
                        e.Graphics.DrawRectangle(Pens.DeepSkyBlue, x * size, y * size, size, size);
                    }
                    else if (cells[x, y] == 1) // 軌跡のセルだったら青い四角(塗りつぶし)を描く
                    {
                        e.Graphics.FillRectangle(Brushes.DeepSkyBlue, x * size, y * size, size, size);
                    }
                    else if (cells[x, y] == 2) // 壁のセルだったら濃い青い四角(塗りつぶし)を描く(追加)
                    {
                        e.Graphics.FillRectangle(Brushes.CornflowerBlue, x * size, y * size, size, size);
                        e.Graphics.DrawRectangle(Pens.Black, x * size, y * size, size, size);
                    }
                }
            }
        }

これで実行すると…

最初のやつに比べると、移動できる範囲が狭くなりました。

この形だと、一筆書きで埋め尽くすことが可能ですね。

続いて、ゴール判定。

やり方は何通りかあるかと思いますが、ここでは単純に「全セルなめて、空白のセルがないことを確認する」というやり方にしたいと思います。

以下のメソッドを追加します。

        private bool IsOver() // 終わったか判定する
        {
            bool result = true;

            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    if (cells[x, y] == 0) // 何もないセルが存在したら
                    {
                        result = false; // まだ終わっていないものとする
                        break;
                    }
                }
            }

            return result;
        }

キー押下イベントの if (move) のところに次のように判定とおめでとうメッセージを追加します。

                if (move)
                {
                    cells[mx, my] = 1;
                    Refresh();
                    e.Handled = true;
                    if (IsOver()) // 判定とおめでとうメッセージの追加
                    {
                        MessageBox.Show("おめでとう!");
                        Init();
                        Refresh();
                    }
                }

これで実行し、無事クリアすると…

おめでとうが表示されましたね!

★ ★ ★

といったところで、今日はここまで。

次回は、もっといろんなパターンの面を準備してみたいと思います。

こういうパズルゲーム、シンプルだけどなかなか奥が深くて好きですねー。

それでは!