MoreBeerMorePower

Power Platform中心だけど、ノーコード/ローコード系を書いてます。

Power Apps で スライドパズルを作成してみた

[2021/08/28:シャッフルのロジックを追記しました]

スライドパズルをPower Appsで作ってみた

こういうやつです。小さいころ、当時の戦隊シリーズのヒーローが書いてあるやつで遊んだなー。

今回はこれをPower Appsで作りました。

固定の画像でもいいんですが、せっかくなので好きな画像で遊べるようにしています。

なお、このアプリを作ろうと思ったきっかけは こだまさん のこちらのアプリを思い出したことです。

powerusers.microsoft.com

作成前にコミュニティのサンプルに投稿していただきました。ありがとうございました!!!

N x N のタイルを作成する

今回のパズルでは任意のNxNのタイルを作れるようにしています。このタイルはGalleryコントロールで表示していますが、まずはGalleryに設定するコレクションを作成します。

例として4x4のタイルを考えます。

f:id:mofumofu_dance:20210827214200p:plain

この時作成するコレクションは下表のようにXY座標と何番のタイルかを格納したものです。

Value x y
1 0 0
2 1 0
3 2 0
4 3 0
5 0 1
6 1 1

...

このようなコレクションはテーブルのクロス結合を使って簡単に作成できます。一般のNの場合には

ClearCollect(indexes,
With({coordinates:
        Ungroup(
            AddColumns(
                RenameColumns(Sequence(N,0),"Value","y"), 
                "add",
                RenameColumns(Sequence(N,0),"Value","x")),
            "add"
        )
},
AddColumns(Sequence(Power(Slider1.Value,2),1),"x",Last(FirstN(coordinates,Value)).x,"y",Last(FirstN(coordinates,Value)).y))
)

Power Appsにおける結合については以前書いたQiitaの投稿をご覧ください。

qiita.com

あとはこのコレクションをGalleryのItemsプロパティに設定します。

あとでタイルの移動を行う都合上、x,yでのソートも含めて以下のように設定します。

SortByColumns(indexes,"y",Ascending,"x",Ascending)

※ここではindexes がコレクション名

さらに、横方向にタイルをN個並べたいので、Galleryコントロールの Wrap count をNにします。

f:id:mofumofu_dance:20210827215118p:plain
4x4のタイルなら、Wrap count = 4

正方形のタイルにしたいので、Galleryコントロールの TemplateSizeのプロパティを Self.Width/4 にします。

これであとはラベルを追加すれば NxN のタイルが出来上がります。

移動ロジックの追加

スライドパズルはどこか1マスを抜いて、そこに隣接するタイルをスライドさせることで遊ぶものです。今回のアプリでは右下 (Value = 16) を空きマスとして扱います。

例えば以下のような配置になったときには、左右上下で隣接しているマスを、16番のマスのほうに移動できます。ほかのマスは移動できません。

f:id:mofumofu_dance:20210827220141p:plain

この判定は以下の式で記述しています。

With({delX:ThisItem.x-LookUp(indexes,Value=16).x,delY:ThisItem.y-LookUp(indexes,Value=16).y}, // クリックしたマスと16番のマスの x,y の差を計算しておく
If(Or(Abs(delX)=1&&Abs(delY)=0,Abs(delX)=0&&Abs(delY)=1), // x の差が1 かつ y の差が0 -> 左右 , y の差が 1 かつ x の差が0 -> 上下 を処理の対象にする
UpdateIf(indexes,Value=ThisItem.Value,{x:ThisItem.x-delX,y:ThisItem.y-delY});UpdateIf(indexes,Value=Power(Slider1.Value,2),{x:ThisItem.x,y:ThisItem.y}))) //自分のxyと16番のマスのxyを交換

これで移動ロジックは完成です。

画像を分割する

今回のアプリでは、利用者がアップロードした画像をNxNに分割してタイルに配置し、スライドパズルを遊べるようにしています。 このために画像を"分割"する必要がありますので、その方法をご紹介します。

端的には、SVGのviewBoxで場所をずらして画像コントロールに表示する です。

SVG の viewBox を活用

SVGの viewBoxタグはSVGに描画するエリアを指定するためのタグです。 SVG内のXY座標とは別に、x, y, height, width の4つのパラメーターで指定された四角い領域を定義することで、座標全体の一部だけを描画させます。

f:id:mofumofu_dance:20210827212626p:plain

例えばSVGの中で画像を表示し、viewBoxをそれより小さく指定すると、下図のように画像の一部分だけを切り取ることができます。

f:id:mofumofu_dance:20210827212645p:plain

もうお気づきでしょう。あとはこれをGalleryコントロールに追加するだけです。

アップロードした画像を SVG で表示する

SVGでの画像の取り扱いについては以前書いたPower Apps上でのSVGでまとめています。

SVG内で画像を表示させるためにはbase64化する必要があります。

mofumofupower.hatenablog.com

今回のアプリでは画像の追加コントロールを使い、その OnChangeプロパティで画像をbase64化しています。

Set(b64,Substitute(JSON(UploadedImage1.Image,JSONFormat.IncludeBinaryData),"""",""))

f:id:mofumofu_dance:20210827221511p:plain

これでb64という変数に画像のbase64文字列が入りました。

タイルに画像を表示

タイルに画像を表示します。最初に作ったコレクションのValueに応じて、画像全体からどこをくりぬくか=viewBoxをどう設定するか を変えていきます。

詳細は割愛しますが、以下の式を使うことでうまいこと切り取れます。

"data:image/svg+xml," &
EncodeUrl("
<svg viewBox='"&Mod(ThisItem.Value-1,Slider1.Value)*Self.Width&" "&(RoundUp(ThisItem.Value/Slider1.Value,0)-1)*Self.Width&" "&Self.Width&" "&Self.Height&"' xmlns='http://www.w3.org/2000/svg'>>
   <image href='"&b64&"' x='0' y='0' width='"&Parent.Width&"' height='"&Parent.Width&"' clip-path='url(#myClip)'/>
</svg>")

ModとかRoundUpしているところでうまくviewBoxのxyを指定しています。

ということでできたものがこちら

f:id:mofumofu_dance:20210827221952p:plain

配置をシャッフルするとそれぞれのタイル上に画像の一部が表示されていることがわかります。

f:id:mofumofu_dance:20210827222016p:plain

配置をシャッフルする

パパセンセイにコメントいただき、解けない配置を考慮していなかったので改めてシャッフルのロジックを考えました。

やることは単純で、16番のマスを上下左右ランダムに100回くらい移動させていくだけです。本当はもう少しまじめな方法があるんですが、Power Appsで作るにはいささか面倒なので簡単な方法をとります。

100回繰り返す処理はタイマーでもいいですし、スライダーを使ったループでも構いません。そのOnTimerEndなどのイベントに以下を設定します。

If(varShuffleCounter>0&&varShuffleCounter<100,
    With(
        {
            //右下のマスのx,yを一時変数に設定
            x16:LookUp(indexes,Value=16).x,
            y16:LookUp(indexes,Value=16).y
        },
        With(
            {
                //入れ替える対象を上下左右から取得し、その中からランダムに1マス決める
                flipTarget:First(Shuffle(Filter(indexes,Or(Abs(x-x16)=1&&Abs(y-y16)=0,Abs(x-x16)=0&&Abs(y-y16)=1))))
            },
            //入れ替え処理
            UpdateIf(indexes,Value=16,{x:flipTarget.x,y:flipTarget.y});
            UpdateIf(indexes,Value=flipTarget.Value,{x:x16,y:y16});
            Set(varShuffleCounter,varShuffleCounter+1)
        )
    )
)

16番に隣接する上下左右のマスはFilter(.....)の部分で取得できます。これをシャッフルして最初の1行をとることで上下左右の中から1マスをランダムに選んでいるわけです。

実際にシャッフルしたところを見てみてください。

もし逆に解けるか不安な場合は、移動した向きの逆を別のコレクションにためておいて、その通り解いてみればいいです。

入れ替え処理の部分に追記します。

            //入れ替え処理
            UpdateIf(indexes,Value=Power(Slider1.Value,2),{x:flipTarget.x,y:flipTarget.y});
            UpdateIf(indexes,Value=flipTarget.Value,{x:x16,y:y16});
            Set(varShuffleCounter,varShuffleCounter+1);
            Collect(moves,{id:100-varShuffleCounter+1,move:Switch(Text(x16-flipTarget.x)&Text(y16-flipTarget.y),"-10","←","10","→","0-1","↑","01","↓")})

これで movesというコレクションをidの順にたどっていけば解けるはずです。 (← は 空白の左にあるマスをタップせよという意味です)

まとめ

今回はSVGのviewBoxとNxNのタイルを使ってスライドパズルを作ったときの要素を紹介しました。

全体として何をしているかはアプリを見ていただいたほうが早いかなと思いますので、ここまでの処理を入れたテストアプリをダウンロードできるようにしました。

ぜひ中をみて、完成させてください!

https://github.com/mofumofu-dance/PowerApps365/raw/master/Samples/SVGsamples.msapp

※シャッフルのロジック追加した版に差し替えました。