numpyを使った敷き詰めパズルの作成について

皆さんは敷き詰めパズルって知っていますか?

有名なモノですと、「明治チョコレートパズル」でしょうか?

www.hanayamatoys.co.jp

指定の枠にブロックを嵌めていって、

全て埋まったらクリアといった代物です。

 

現在私は新しくゲームを作っているのですが、

その中のギミックに敷き詰めパズルに

近いものを組み込もうと考えています。

 

なので、試作品として敷き詰めパズルを作ろうと思ったのですが、

私の貧弱な検索力じゃ、敷き詰めパズルの

ゲームとしての実装例が出てこなかったのです。

 

どうも、アルゴリズムの問題で取り扱いやすい問題のようで、

出てくる実装例がゲームとしてでなく、

「解の組み合わせを求めるプログラム」が沢山出てくるのですよ。

 

こりゃ1から作らんとダメか、と思い

CLI上で動く、敷き詰めパズルを作ってみることにしました。

 

この際、行列計算が得意なnumpyを使ったところ、

結構便利にできたので、その機能も軽く紹介しながら、

どんな感じにゲームを作っていったのか紹介できればなと思います。

 

できたゲーム

こんな感じになりました。

f:id:cacapon:20210604103955g:plain

 

ゲームの主な材料

  • stage area
    • ピース(ブロック)を置くエリア
    • すべて埋まったらクリアとする
  • ctrl area
    • ピースを持っているエリア
      持っているピースを移動したり
      入れ替えたり
      回したり反転させたりできる。
  • ピース
    • 3つで構成されるI型とL型のピースがある  

実装について

見たい方はこちらをクリック(200行未満のコード)

Numpyの恩恵を大きく受けた操作についての解説

ピースの回転

ピースの回転はnumpy.rot90(m, k=1)を使いました。

これは、np.ndarrayの行列mを90度半時計周りにkの数値だけ回す機能になります。

因みに3次元とか次元が増えると試してないのでわかりません。悪しからず

今回は時計回りに回したかったので、kの値を-1で固定して使用しました。

ピースの反転

反転はピースを裏返しにする機能ですね。

テトロミノでいう、JミノとLミノ、

SミノとZミノを入れ替えると思っていただければ。

テトロミノが分からない方はテトリスのブロックを思い浮かべてください、アレです。


ピースの反転にはnp.fliplr(m)を使いました。

この機能は、左右に入れ替える機能ですね。


因みに、今回の3つのブロックで構成されるIとLピースでは

回転で事足りるので不要な機能なのですが、

実際のゲームで数の多いピースを作る予定のため、確認の為にも追加しました。

ピースの移動

ピースの移動自体は、np.roll(a, shift[, axis])を使いました。

aで指定された配列をシフト分だけ動かした配列を返す機能です。


例えば今回使った感じですと、

np.roll(self.controll_area, (self.piece_pos['y'], self.piece_pos['x']), (0, 1))なので、

ctrl areaをピースの位置であるyとxの位置までシフトさせる(0,1は軸を指定するタプル)

といった意味になります。


因みに、np.rollはぐるっと回る(例:np.roll([1,2,3],1) -> [3,1,2])ので、

別の関数で移動量の上限を決めたり、

回転させたりした際に、移動量を補正したりして、

ピースが枠より大きく移動しないようにしています。

置けるかチェック

stage_area に ctrl_areaのピースを置くのは、numpyの行列の判定を使いました。

重なっていたらピースは置けないので、

stage_areaとctrl_areaの各要素をNANDで判定できれば、置けるか分かります。*1


numpyはそのあたりの判定がとてもやりやすく出来ていまして、

A,Bがboolの行列なら~(A & B)で各要素ごとの判定結果が行列で返すことができます。

bool型の行列でなくとも、 A == '' のような条件式で、bool型の行列に変換することができます。

また、X.all()で全ての要素がTrueなのかを取得することも容易です。*2


これらの機能を使って、

今回の例では下記のように判定することにしました。

  1. X != '' で空白じゃないかを調べ、bool型の行列にする
  2. bool型の行列にしたctrl_area と stage_area をNANDで判定する。
  3. .all()全てTrueか取得する。

ピースを置く

ピースを置くイメージは、二つの同じ大きさの行列を重ね合わせて

空いているところに、新しいピースを置いていくイメージでしょうか。

f:id:cacapon:20210604115934p:plain


このイメージを実現するために

numpy.where(condition[, x, y]) を使いました。

この機能はconditionに記載された真偽に対応して、

Trueならx,Falseならyを当てはめていきます。*3

今回の例では、np.where(self.controll_area != '', self.controll_area, stage)として、

ctrl area が埋まっている部分(ピース)はctrl areaに、

それ以外の部分はstage areaとすることで、

ピースを置くのを表現しました。

わざわざctrl ereaとpieceを分けて考えているわけ

今回の実装を見て頂くと

ピースとctrl area 、そして座標は分けて管理しています。

何でそんなことをしているかというと、

最初に、ctrl area上に置いたピースが、理想の動作をしなかったので、

四苦八苦した結果になっています。

結局落ち着いた形として、

  1. ピースの回転、反転はピース自体に対して行う
  2. 移動はxy座標で管理。上限超えた場合は直接修正する
  3. 見える形になって初めてピースをctrl areaに置き
    座標まで移動させてから、写す
  4. 処理が終わったら ctrl areaを空行列にする

といった流れになりました。

そのせいか、初期化処理、終了時用の関数が処理が所々入っています。

したみたいな実装にできれば、スマートになったかなと思うのですが、

出来るのですかね? この辺りは要勉強でしょうか。

def init():
    #~~~~
def final():
    # ~~~~

def func_in_init_final(func):
    init()
    func #ここを引数で希望の関数と置き換えたい
    final()

ステージに置いたブロックを取り除くremoveについて

実はこれ未完成で、

指定した座標に置いたブロックを取り除くのを想定して言うのですが、

同じ型のブロック全てを取り除いてしまっています。

実際のゲームでは、ブロック名を一意にすることで、対応する予定ですが、

試作品なのでまあいいかと妥協しました。

終わりに

取りあえず、思いつく部分は書いてみたのですが、参考になりましたでしょうか?

この後はUnityでUIとして動作する敷き詰めパズルを作っていきたいなと思います。

このあたりの知見はまだ少ないので、別の苦労をしそうです。

あと、numpyで簡単にできていた部分をC#でどう表現するかも苦戦するかもです…


また、パズル以外にも戦闘も入れる予定なので、

戦闘シーンの作成やアニメーション、なども行っていく予定です。

色々盛りだくさんですが、ぜひ完成まで進めていきたいと思います。

出来上がりましたら紹介しますね。

*1:0が空き、1が埋まるとすると、(0,0)->T (0,1)->T (1,0)->T (1,1)->F で置けるか判定できると考えました。

*2:一つだけTrueならTrueになる.any()というのもあります。

*3:x,yを指定しない場合、条件に当てはまったインデックスを取得します。今回は使いませんでしたが、便利そうな機能です。