【Power Apps】ルービックキューブ的なミニゲームを作成

## 概要

**Power Apps でルービックキューブを再現した投稿を拝見**しました。

https://qiita.com/akira_365/items/1c4ee1bbce66acac2a2c

これを参考に、**太閤立志伝Ⅴの礼法師事のミニゲーム**を Power Apps で再現してみました。

## 画面

**ルービックキューブの 1 面揃えのようなミニゲーム**です。ルービックキューブと比較すると、背面や角の処理を無視できるので、断然シンプル。

https://x.com/anpanda_44075/status/1888684135822365161

実際にはサウンドも鳴らしてます。

## 主な仕様

* マスは、縦方向・横方向に循環する
* マスを移動させた回数をカウントする
* 中央の 9 マスがすべて同じ色になったらゲームクリア
* 難易度は 4 段階
* マスの色数は、難易度に 1 を足したもの（例：難易度 2 のとき、3 色）

## ロジック

### ランダムに色分けされた 45 マスを作成

```
ClearCollect(
    colABC,
    Shuffle(Sequence(45, 65, RoundUp((varLevel + 1) / 45, 6)))
);
```

下準備として、**色数を 45 で割った値を増分とする 45 個の配列 colABC を `Sequence` で作成** します。3 色の場合、`3 / 45 ＝ 1 / 15` ずつ値が増えていき、16 番目で 1 の位が繰り上がります。

**初期値の 65 は、`Char` で文字に変換すると A** になります。1 の位が繰り上がるごとに A, B, C... と変化します。

また、`RoundUp` をしているのは、小数点以下の計算が入る都合なのか、5 色目が 8 マスになってしまったため。よくわかってないので深掘りしないと。

`Shuffle` で並びをランダムにして、下準備は完了。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/7be4ec8d-6e21-5a21-f53a-31efd79e971f.png)

続いて、**ゲーム中のマス状態を表す、 45 個の要素を持つ配列 colData を作成**します。固定された Value列 (1 ～ 45) と、マスの色を表す clr 列を持ちます。

```
ClearCollect(
    colData,
    AddColumns(
        Sequence(45,1,1),
        clr,
        Char(RoundDown(Index(colABC, ThisRecord.Value).Value, 0))
    )
);
```

clr 列は、下準備で作成した colABC から `Index` で同じ行数の値を取得し、小数点以下を切り捨てたものを `Char` でアルファベットに変換しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/e95a9cab-eee3-883b-9ef1-bfa569a17112.png)

### 画面上のマス表示

**ゲーム画面の各マスの表示は、3 つのギャラリーを組み合わせて構成**しています。

中央横の 27 マスは、colData から 1 ～ 27 を取得してくるだけ。

```
Filter(colData, Value <= 27)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/32e48a32-a430-911d-c76f-2e82ece1cccf.png)

上と下の各 9 マスは、条件を指定して必要な値だけを取得しています。また、数値が縦方向に並ぶように、折り返し 3 の水平ギャラリーを用いています。縦に並べたいときに水平とはこれいかに。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/2cbbc24f-bdaa-2fa4-f4b4-8f4a3055a140.png)

```
// 上の 9 マス
Filter(colData,
    Value >= 28 && Value <= 30 ||
    Value >= 34 && Value <= 36 ||
    Value >= 40 && Value <= 42
)
```

```
// 下の 9 マス
Filter(colData,
    Value >= 31 && Value <= 33 ||
    Value >= 37 && Value <= 39 ||
    Value >= 43 && Value <= 45
)
```

**各マス(ギャラリーの要素) には四角形を置いて、そのマスの clr によって色を動的に変化**させます。

```
Switch(ThisItem.clr,
    "A", Color.LightGreen,
    "B", Color.Plum,
    "C", Color.Khaki,
    "D", Color.LightBlue,
    "E", Color.Ivory,
    "Z", RGBA(0,0,0,0), //完成後は透明に
    Color.LightGray
)
```

### 移動ボタン：左方向

左上の左向き矢印を例に解説します。

処理としては、**1 の clr が 、もともと 2 にあった clr に置き換わる**、というものです。元のマスより 1 大きいマスの clr に置き換わる、と表現できます。ただし、9 のマスに関しては、循環して 1 にあった clr に置き換わります。

```
// 左上の左向き矢印
ClearCollect(colDataSampling, colData);
UpdateIf(
    colData As Data,
    Data.Value = 9, {clr:LookUp(colDataSampling, Value = 1).clr},
    Data.Value >= 1 && Data.Value <= 9, {clr:LookUp(colDataSampling, Value = Data.Value + 1).clr}
);
Select(Button_judge)
```

まず、**移動前の状態を colDataSampling として保存**しておきます。移動後の状態を参照してしまうと、循環する部分(1 と 9) が常に同じ値になってしまいます。

**例外処理である 9 のマスの処理を行ってから、それ以外(1 ～ 8) の通常処理**を行っています。通常処理の条件 `Data.Value <= 9` だと 9 のマスも含まれていますが、`Data.Value = 9` のところで先に処理されているため、無視されるようです。`UpdateIf` の挙動を確認したかったのでこの形にしていますが、本来は `Data.Value < 9` の方がよいと思われます。

マスを移動させたあと、`Select(Button_judge)` でゲームクリア判定を行うボタンを実行しています。

### 移動ボタン：右方向

続いて、右上の右向き矢印の場合。

左上の左向き矢印と異なるのは、**例外処理の対象と、通常処理の更新後の参照先**です。例外処理は 1 と 9 を入れ替え、通常処理は ＋ を － に替える形です。

```
// 右上の右向き矢印
ClearCollect(colDataSampling, colData);
UpdateIf(
    colData As Data,
    Data.Value = 1, {clr:LookUp(colDataSampling, Value = 9).clr},
    Data.Value >= 1 && Data.Value <= 9, {clr:LookUp(colDataSampling, Value = Data.Value - 1).clr}
);
Select(Button_judge)
```

### 移動ボタン：上方向

左右方向の矢印は変更対象の数値が連続しているのでシンプルでしたが、**上下方向の移動は数値が連続していないので複雑**になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/de60e8bc-79d1-c0e5-8f81-09d0973086ae.png)

左上の上向き矢印を例に解説します。

```
// 左上の上向き矢印
ClearCollect(colDataSampling, colData);
UpdateIf(
    colData As Data,
    // 中央9マスに含まれるマス
    Data.Value = 4, {clr:LookUp(colDataSampling, Value = Data.Value + 9).clr},
    Data.Value = 13, {clr:LookUp(colDataSampling, Value = Data.Value + 9).clr},
    Data.Value = 22, {clr:LookUp(colDataSampling, Value = Data.Value + 9 + 5 * 0).clr},
    // 他の9マスから値を取得する
    Data.Value = 30, {clr:LookUp(colDataSampling, Value = Data.Value - 26 - 5 * 0).clr},
    Data.Value = 33, {clr:LookUp(colDataSampling, Value = Data.Value - 5).clr},
    // それ以外
    Data.Value >= 28 && Data.Value <= 33, {clr:LookUp(colDataSampling, Value = Data.Value + 1).clr}
);
Select(Button_judge)
```

**28, 29, 31, 32 は連続した数値の中での通常処理が可能で、それ以外は例外処理**として扱います。例外処理は基本的には力業で、画面上の数値を見ながら、適切な clr を取得してくるように設定しつつ、ある程度の一般化を試みました。

**中央 9 マスに含まれるマス(4, 13, 22) については、そのマスより 9 大きいマスから取得**。

ただし下段のマス(22) については、縦 2 列目の 23 は 37 から、縦 3 列目の 24 は 43 から取得することになります。縦 n 列目のとき、元のマスに対して `9 + 5 * (n - 1)` を足す形で一般化できました。

**中央 9 マスから値を取得するマス(30) についても、縦 n 列目のとき、元のマスに対して `26 - 5 * (n - 1)` を足す**形で一般化できました。最下段のマス(33) は、そのマスより 5 小さいマスを取得すれば OK。

### 移動ボタン：下方向

左下の下向き矢印の処理を記載しました。上向き矢印の逆の処理をしている感じなので、解説は省略します。

```
// 左下の下向き矢印
ClearCollect(colDataSampling, colData);
UpdateIf(
    colData As Data,
    // 中央9マスに含まれるマス
    Data.Value = 4, {clr:LookUp(colDataSampling, Value = Data.Value + 26 + 5 * 0).clr},
    Data.Value = 13, {clr:LookUp(colDataSampling, Value = Data.Value - 9).clr},
    Data.Value = 22, {clr:LookUp(colDataSampling, Value = Data.Value - 9).clr},
    // 他の9マスから値を取得する
    Data.Value = 28, {clr:LookUp(colDataSampling, Value = Data.Value + 5).clr},
    Data.Value = 31, {clr:LookUp(colDataSampling, Value = Data.Value - 9 - 5 * 0).clr},
    // それ以外
    Data.Value >= 28 && Data.Value <= 33, {clr:LookUp(colDataSampling, Value = Data.Value - 1).clr}
);
Select(Button_judge)
```

### マス移動後の処理

**移動ボタンを押してマスを移動させたあとに押される、Button_judge の処理**です。

```
Set(varCount, varCount + 1);
If(
    Index(colData, 4).clr = Index(colData, 5).clr && 
    Index(colData, 4).clr = Index(colData, 6).clr &&
    Index(colData, 4).clr = Index(colData, 13).clr &&
    Index(colData, 4).clr = Index(colData, 14).clr &&
    Index(colData, 4).clr = Index(colData, 15).clr &&
    Index(colData, 4).clr = Index(colData, 22).clr &&
    Index(colData, 4).clr = Index(colData, 23).clr &&
    Index(colData, 4).clr = Index(colData, 24).clr,
    Set(flgPanelErase, true)
)
```

**中央の 9 マスにあたるレコードがすべて同じ clr であるとき、ゲームクリア**になります。これまた力業で、中央の 9 マスのうち、左上(4) と他の 8 マスの clr が同じかどうかを判定しています。

**移動回数に 1 を足すのと、ゲームクリア時のみ flgPanelErase を true に**しています。

### ゲームクリア時の処理

ゲームクリア時、**各マスがランダムで消えていき、背景の寺社画像が徐々に見えてくるような動き**を設定しました。

まず、マス生成時に **1 ～ 45 をランダムに並べたコレクション colRandNum を作成**しておきます。

```
ClearCollect(colRandNum,
    Shuffle(Sequence(45, 1, 1))
);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/ab391924-dd71-423f-3757-b1e06ccf5769.png)

このコレクションを上から順番に見ていき、**colData から同じ Value を持つマスの色を消す、という動きを繰り返す**ようにしました。

**繰り返し処理には、Duration を短く(50ミリ秒) したタイマー**を利用します。AutoStart で flgPanelErase を参照して、ゲームクリア時にタイマーが開始されるようにして、繰り返し実行する処理を OnTimerEnd に記載します。

```
// OnTimerEnd
UpdateIf(colData,
    Value = First(colRandNum).Value,
    {clr: "Z"}
);
Remove(colRandNum, First(colRandNum));
If(IsEmpty(colRandNum),
    Set(flgPanelErase, false)
)
```

**colRandNum の 1 レコード目の Value と 同じ Value を持つ colData の clr を "Z" に変更**します。各マスは clr が "Z" の時に Fill, BorderColor を透明にするようにしています。

その後、**参照していた colRandNum の 1 レコード目を削除し、colRandNum が空になっている場合は flgPanelErase を false に変更**。flgPanelErase が AutoStart のフラグになっているので、false になると繰り返しが停止します。

### 背景画像のマスク

**背景の寺社画像にセピア色の四角形を重ねて、ゲームクリアの繰り返し処理が進むごとに透明度を上げる**ことで、画像が徐々に明るくなるようにしました。colRandNum が 1 レコードずつ削除されていくので、そのレコード数を `CountRows` で参照し、`RGBA` の透明度に利用しています。

```
// Fill
RGBA(74, 59, 42, 0.7 * CountRows(colRandNum) / 45)
```

### おまけ：鑑賞モード

**12 個の移動ボタンをランダムでクリックし続ける**ようになるスイッチを仕込みました。ルービックキューブについて調べているうちに、ランダムで回し続けていれば猿でもいつかは揃えられる「無限の猿定理」なるものがあると知り、試してみたくなったので。

https://qiita.com/yusuke_s_yusuke/items/044c2e4e2ccbb949511a

**タイマーの OnTimerEnd に、ランダムで 1 ～ 12 の数値を生成し、対応する移動ボタンを押させる処理**を書き、ゲームクリアにならない限り Repeat させています。

```
Switch(RandBetween(1, 12),
    1, Select(Button_L1),
    2, Select(Button_L2),
    3, Select(Button_L3),
    4, Select(Button_R1),
    5, Select(Button_R2),
    6, Select(Button_R3),
    7, Select(Button_T1),
    8, Select(Button_T2),
    9, Select(Button_T3),
    10, Select(Button_B1),
    11, Select(Button_B2),
    12, Select(Button_B3)
)
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/a327affa-9748-d69b-a791-8fff9d432fbe.png)

↑ 猿が 2 色を攻略した瞬間。難易度 1 ＝ 2 色でもなかなか揃わないもんですね。


## 気づき

* `Sequence` で作られる配列の値は、Value で参照できる
* `UpdateIf` に複数の条件を記載した場合、条件 1 に合致すると条件 2 の処理は行われない
* 繰り返し処理はタイマーで
* 力業で処理を書いてから、一般化できるか検討してみる

冒頭で紹介したとおり、他の記事で作り方のヒントを得られたおかげで、そこまで苦戦せずに作れたと思います。ゲームクリア時の処理は蛇足ですが、他のゲームにも応用できそう。ランダムではなく、渦巻状に消えていくとかも可能ですね。

太閤立志伝Ⅴのミニゲーム、他にも再現したいものがあります。開墾がラスボスで、他にも軍学・算術・医術・茶道。放物線の軌道計算ができれば、弓術も再現してみたいところです。