文系人間がプログラミングをはじめてみた。

プログラミング初心者のゼロからの勉強記録。

マクロで作ったマインスイーパーの解説 詳細編 ゲーム内の操作を行う2

どうもこんにちは。

 

今回もマクロで作ったマインスイーパーのコード解説をしていきます。

どんな感じのマインスイーパーになっているかはこちらからご確認ください。

programminghajimetemita.hatenablog.com

 

今回は前回の記事の続きになります。

マスを開く処理のうちの、空白マスを開いたときの処理です。

前回の記事はこちらからどうぞ。

programminghajimetemita.hatenablog.com

 

では内容に入ります。

 

 

1.コードの内容

まずは実際に記述したコードの内容について紹介します。

なお、マスを開く処理を制御するマクロ(マクロ名operation)では、

空白マスを開いたときに上記マクロを呼び出しています。

参考までに関連するコードを以下に掲載します。

詳しく知りたい方は前回の記事をご覧ください。

 

2.マインスイーパーの挙動

空白マスを開いたときの挙動は以下の2つに整理できます。

・空白マスの周囲マスをすべて開く

・周囲マスに空白マスがあれば当該マスの周囲マスをすべて開く

 

このように書くとシンプルに見えますが、

これをプログラム化するのにはかなり骨が折れました。

(以下、挙動に関係ない、プログラムを作ったときの感想です。
興味のない方は飛ばしてくださいね。)

 

ユーザーが選んだマスを起点に処理を行うのですが、

どのマスを起点にしたとしても上記挙動に沿う処理にしないといけません。

 

また、空白マスの位置も上下左右に不規則に連続しているので、

空白マスが連続する地帯がどんな形であったとしても上記挙動に沿う処理にしないといけません。

 

上記のように不規則なものをどうやって規則的なプログラムにするのか、かなり悩みました。マクロでテトリスを作ったときよりも悩んだぐらいです。

 

作成期間中は寝ても覚めてもマインスイーパーのことを考えるぐらいに考えて、

ようやくたどり着いたのが今回紹介するコードになります。

 

要は、かなり頑張って作ったコードなんですよ、ということです。笑

それをお伝えしたくて作成時の感想を述べました。

駄文失礼しました。ではコードの内容に入っていきましょう。

 

3.コードの内容解説

空白マスを開いたときの処理を行うマクロでは、変数r, cを引数に設定しています。

ここで、変数r, cとはユーザーが選んだマスの位置(rは行番号、cは列番号)を示す変数です。

r, cにユーザーが選んだマスの位置を登録する処理は前回記事で解説していますので、

興味のある方は前回の記事をご覧ください。

 

なぜr,cを引数に設定しているかというと、

このマクロ内でユーザーが選んだマスの位置を起点に処理を行わせるためです。

引数に設定することで、別のマクロで登録したユーザー選択マスの位置情報を、

このマクロに連携しているということです。

 

変数の宣言です。

find_cellはRange型のオブジェクト変数です。

検索(Find)の処理を行うときにこの変数を準備する必要があり、

このマクロ内で検索の処理が登場するのでこの変数を宣言しています。

 

blk_addはVariant型の変数です。

このマクロ内でDictionaryというオブジェクトが登場するのですが、

そのDictionaryに値を登録するためにこの変数を準備する必要があるためここで宣言しています。

 

なお、変数や定数は基本的に別の場所で宣言を行っているのですが、

この2つの変数のみマクロ内で宣言を行っています。

こうしないと処理がうまくいかなかったので、特別扱いしています。

 

なぜ処理がうまくいかなかったのかはわからずじまいです。

結果よければよし、ということで原因解明はしませんでした。

 

Dictionaryというオブジェクトを利用するために行っている処理です。

blk_listというオブジェクト変数にDictionaryオブジェクトを登録する処理になります。

 

2.で触れたとおり空白マスを開いたときには、

・その空白マスの周囲マスをすべて開く

・周囲マスに空白マスがあればそのマスの周囲マスもすべて開く

必要があります。

 

つまり、空白マスに隣接する空白マスすべてに対して、周囲マスを開く処理が必要になるわけです。

 

これを実現するために、

「開いた空白マスの周囲にある空白マス、そのまた周囲にある空白マスをすべてリストに登録し、

登録完了後にすべての空白マスに対して周囲マスを開く処理」

を行わせればよいのではないか、と考えました。

 

この処理のうち、空白マスをリストに登録する、という部分にDictionaryを採用しました。

Dictionaryを使えば、任意の値をリスト化できますし、

登録する値の重複を避けることもできるので、

今回の処理にぴったりの処理になっています。

 

ユーザーが選んだマスの周囲の範囲を対象に、空白マスを検索する処理です。

この処理により、先ほど宣言した変数find_cellに、検索でヒットした空白マスが登録されます。

 

検索でヒットした空白マスをDictionaryに登録する処理です。
一行目の条件分岐では、検索で空白マスがヒットしたかどうかを判定しています。

空白マスがヒットしていればThen以下の処理が実行されます。

 

二行目の条件分岐ではヒットした空白マスがDictionaryに登録済みのマスかどうかを判定しています。

未登録の空白マスであればThen以下の処理が実行されます。

 

三行目の処理はヒットした空白マスの値を" (半角スペース)"に変更する処理です。

登録対象になる空白マスを探すのにFind(検索)処理を行っており、

当該処理で一度ヒットしたマスが再度ヒットすることのないようにこの処理を行っています。

この処理を入れておかないと、同じマスが検索処理でヒットし続けることになるのでご注意ください。

 

四行目の処理がDictionaryに登録する処理になります。

登録時にはキーと値の二つを登録する必要があり、

キーに空白マスのアドレスを、値に空白マスの値を登録しています。

 

このうち重要なのはキーの方です。

登録完了後、登録したすべてのマスに対して周囲のマスを開く処理を行わせるのですが、

この処理を行うときにキーとして登録したアドレスの情報を利用することになるためです。

 

空白マスをDictionaryに登録するマクロを呼び出す処理です。

先ほどの処理で空白マスの登録を行いましたが、

網羅的に登録するのはこちらのマクロを利用して行います。

以下、このマクロのコード解説に移ります。

 

空白マスを網羅的に登録するマクロ"rec_blank"では、

オブジェクト変数"blk_list"を引数に設定しています。

先ほどのマクロで利用したDictionaryをこちらのマクロでも利用するためです。

 

二行目・三行目では先ほどのマクロで宣言した変数と同じ変数を宣言しています。

趣旨は先ほど解説した内容と同じです。

 

空白マスを網羅的に登録するために行う繰り返し処理になります。

Dictionaryに登録したマスを起点に繰り返し検索を行うことで、

周囲にある空白マス、そのまた周囲にある空白マスを網羅的に登録していきます。

 

一度目の処理では、Dictionaryに登録されたマスは一つだけ(先ほど解説した処理で登録したもの)ですが、

後述する"再帰呼び出し"を行うことで、Dictionaryに登録されるマスが増えていくので、

網羅的な登録が可能になります。

 

このあたりは再起呼び出しの処理解説時にもう少し詳しく解説したいと思います。

ここでは、「Dictionaryに登録されたマスすべてに対して、周囲にある空白マスを検索する処理を行う」という点をおさえてもらえればと思います。

 

Dictionaryに登録されたマスの周囲マスにある空白マスを検索する処理です。

Rangeのカッコ内にある"blk_add"はDictionaryに登録されたマスのキーを指す変数です。

つまり、これは登録されたマスのアドレス(セル番地)を示しています。

 

Dictionaryへの登録時に、キーとしてアドレスを登録しておくことで、

登録されたマスへの処理を簡単に行うことができるようになります。

 

Offsetで指定しているのは登録されたマスの左上のマスと右下のマスです。

このように指定を行うことで登録されたマスの周囲を範囲指定することができます。

 

繰返しになりますが、この処理によってDictionaryに登録されたマスとその周囲マスを対象に空白マスを検索しています。

 

Dictionaryに登録されたマスの周囲にある空白マスのすべてをDictionaryに登録する処理です。

空白マスの登録が終わるまで繰り返し処理が行われます。

 

まず、二行目にある条件分岐で、空白マスがヒットしたかどうかを判定しています。

空白マスがヒットしていれば三行目の条件分岐に移ります。

なお、空白マスがヒットしなければこの繰り返し処理を終了します(Else以下の部分)。

 

三行目の条件分岐ではそのマスがDictionaryに登録されたマスかどうかを判定しています。

Dictionaryに登録されたマスでなければ四行目以下の処理を行います。

登録済みであれば、同じ範囲に対して再度空白マスの検索を行います(End If以下の処理)。

 

四行目・五行目の処理は検索でヒットしたマスをDictionaryに登録する処理です。

処理の内容は先ほど解説したものと同じなのでここでは説明を割愛します。

 

六行目の処理が、先ほど少し触れた"再起呼び出し"の処理になります。

Callで自マクロを呼び出すことで、自マクロの処理を繰り返し行うことができます。

 

ここでは、"rec_blank"マクロ内で、"rec_blank"を呼び出し、

"rec_blank"の処理を再度、最初から行っています。

 

ここまでの処理をまとめると以下になります。

 

①Dictionaryに登録されたマスの周囲に空白マスがあるか検索

 ヒット⇒②へ

 ヒットなし⇒④へ

②検索でヒットした空白マスがDictionaryに未登録であれば、
そのマスを登録し、再度①を実行

③登録済みであれば、同じ範囲で再度空白マスを検索

 ヒット⇒②へ

 ヒットなし⇒④へ

④Dictionaryに登録された別のマスに対して①~③実行

⑤Dictionaryに登録されたすべてのマスに対して①~③処理完了後、
このマクロの処理を終了

 

このように再起呼び出しで処理を繰り返していくことで、

連続する空白マスがどのような形になっていようとも、

周囲にある空白マスをすべてDictionaryに登録することができます。

 

以上で空白マスを登録するマクロ"rec_blank"の解説を終わります。

では再び空白マスを開いたときの処理を行うマクロ"act_blank"に戻ります。

 

Dictionaryに登録された空白マスの周囲マスを開く処理です。

登録されたすべてのマスに対して開く処理を行うために、

繰返処理For Eachを使っています。

 

対象となるリストは空白マスをすべて登録したDictionary(blk_list)です。

Dictionaryに登録されたマスのキー(blk_add)を利用して開く処理を行っていきます。

 

登録されたマスの周囲を含めた範囲を指定するために、

ここでもOffsetを利用しています。これで空白マスの周囲マスを範囲指定できます。

 

With~End Withで囲まれた部分の処理が開く処理です。

まず、オープン済みとわかるように、Interior~で背景色を変更します。

その後、マス内の値を見えるようにするため、NumberFormatLocalで表示形式を変更します。

空白マスの周囲マスには数字マスが出てきますので、表示形式の変更は必須です。

 

以上でコードの内容解説を終わります。

 

4.特に重要なコード

3.で解説したコードのうち、特に重要と考えるものについて振り返りたいと思います。

 

・空白マスをリスト化する

Dictionaryオブジェクトを利用します。

.AddでDictionaryへの登録、.exists()でDictionaryに登録済みかのチェックができます。

 

.Addで登録する情報は任意に指定することができます。

今回紹介したように、空白マスのアドレス(.Addressと記述しています)を登録しておくことで、

そのマスに対する処理が行いやすくなります。

 

・周囲にある空白マスを網羅的に登録する

再起呼び出し(Call 自マクロ名)を利用します。

これを利用することで、処理の途中で最初に戻って処理を行わせることができます。

 

通常の繰返処理(ForやDo)では、そのコードの中に記述した処理を最初から最後まで実行することを繰り返すことになりますが、

再起呼び出しでは処理の途中で、以後の処理を中断して最初に戻って処理させることができるので、

その点が通常の繰返処理と異なる点になるかと思います。

 

処理の途中で最初から戻って再処理させたい場合には、

この再起呼び出しという処理を使ってみてください。

 

5.おわりに

今回は空白マスを開いたときの処理について解説しました。

次回はフラグを設置する処理について解説しようと思います。

ではまた。