【Power Apps】「どうぶつしょうぎ」を作ってみた

## 概要

長女と時々遊んでいる **「どうぶつしょうぎ」を、Power Apps で再現**してみました。

どうぶつしょうぎのルールは下記を参照ください。

https://www.shogi.or.jp/column/2016/11/post_43.html

## 画面

3×4の盤面を用意して、各プレーヤーの駒を配置しています。各プレーヤーの持ち駒を表示する欄も作ってあります。

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

駒の画像は、いらすとやの画像を使って PowerPoint で作成しました。

## スタートボタン押下時

**初期設定として、コレクション・変数を作成**します。

### 盤面情報
row, col でマスの位置、selectable で選択可否、piece で駒の種類、owner でその駒の持ち主を示しています。
```js
ClearCollect(colBoard,
    {row:1, col:1, selectable:false, piece:"きりん", owner: 1},
    {row:1, col:2, selectable:false, piece:"らいおん", owner: 1},
    {row:1, col:3, selectable:false, piece:"ぞう", owner: 1},
    {row:2, col:1, selectable:false, piece:Blank(), owner: 0},
    {row:2, col:2, selectable:false, piece:"ひよこ", owner: 1},
    {row:2, col:3, selectable:false, piece:Blank(), owner: 0},
    {row:3, col:1, selectable:false, piece:Blank(), owner: 0},
    {row:3, col:2, selectable:false, piece:"ひよこ", owner: -1},
    {row:3, col:3, selectable:false, piece:Blank(), owner: 0},
    {row:4, col:1, selectable:false, piece:"ぞう", owner: -1},
    {row:4, col:2, selectable:false, piece:"らいおん", owner: -1},
    {row:4, col:3, selectable:false, piece:"きりん", owner: -1}
);
```
owner は、**下から上に攻めるプレイヤーを -1、上から下に攻めるプレイヤーを 1** としています。こうしておくと、「ひよこ」の row にプレイヤーの番号(1/-1)を足すことで、「ひよこ」の進めるマスを割り出すことができます。プレイヤーの手番交替の際は、プレイヤー番号に -1 を掛け算することで、1/-1 を切り替えることができます。

### 持ち駒情報
```js
Clear(colCaptured);
/*
//持ち駒追加テスト
Collect(colCaptured,
    {owner:-1, piece:"ひよこ", using: false, id:1},
    {owner:-1, piece:"きりん", using: false, id:2},
    {owner:1, piece:"ひよこ", using: false, id:3}
);
*/
```
持ち駒情報には、**利用中状態を示す using と、一意の id を設定**しています。id を設定することで、同じ駒を 2 つ持っているときに、どちらを使ったかが判別できるようになります。使った持ち駒を削除する際に、LookUp 関数で参照すると 1つ目の駒が削除されてしまい、2つ目の駒をクリックしたときの見え方が気持ち悪かったため。

また、プレイヤーごとに持ち駒情報を分けずに、**両プレイヤーの持ち駒をひとつのコレクションに格納**しています。owner 列の値でどちらのプレイヤーの持ち駒かがわかるようにしてあります。

### 変数

```js
UpdateContext(
    {   
        locActivePlayer: -1,
        locMovingPiece: {row: Blank(), col: Blank(), piece: Blank(), owner: Blank()},
        locWinner: Blank(),
        locUsingCaptured: false
    }
);
```

**locMovingPiece は、移動中の駒の情報を表す配列**です。駒の移動は「駒を選択する→移動先の駒をクリックする」の 2段階になるので、移動元のマスの情報を記録しておく必要があります。

他の説明は割愛。

## 主なロジック

### マスをクリックした時

```mermaid
graph TD
	start[/マスをクリックした\] --> if1
  if1{{自分の駒がある？}} -- true --> sele[選択した駒の移動可能範囲を選択可能に]
	  sele --> mov[\選択した駒を「移動中の駒」として記録/]
  if1 -- false --> if2{{選択可能なマス？}}
	  if2 -- true --> if3{{相手の駒がある？}}
		  if3 -- true --> cap[選択した駒を持ち駒に追加]
			  cap --> if4{{選択したマスの駒が「らいおん」？}}
			  if4 -- true --> win[\自分が勝利/]
			  if4 -- false --> if5{{選択したマスの駒が「にわとり」？}}
				  if5 -- true --> niwatori[持ち駒の「にわとり」を「ひよこ」に変更]
					  niwatori --> move[「移動中の駒」の情報を選択したマスに上書き]
					  move --> if7{{選択したマスが最終列？}}
					  if7 -- true --> if8{{「移動中」の駒は何？}}
						  if8 -- ひよこ --> niwatori2[選択したマスの駒を「にわとり」に変更]
							  niwatori2 --> delbefore[「移動中の駒」のマスの情報を削除]
							if8 -- らいおん --> win
							if8 -- それ以外 --> delbefore
						if7 -- false --> delbefore
					  delbefore --> delmove[「移動中の駒」の情報を削除]
					  delmove --> if6{{「持ち駒利用中」？}}
					  if6 -- true --> delcap[「利用中の持ち駒」と同じ駒を持ち駒から削除]
						  delcap --> delcap2[「利用中の持ち駒」の情報を削除]
						  delcap2 --> disable[すべてのマスを選択不可能に]
						  disable --> change[\手番交替/]
						if6 -- false --> disable
				  if5 -- false --> move
			if3 -- false --> move
	  if2 -- false --> delmove2[「移動中の駒」の情報を削除]
		  delmove2 --> disable2[\すべてのマスを選択不可能に/]
```

### 持ち駒をクリックした時

```mermaid
graph TD
	start[/持ち駒をクリックした\] --> enable[駒がないマスを選択可能に]
	enable --> using[選択した持ち駒を利用中状態に]
	using --> using2[選択した持ち駒を「移動中の駒」として記録]
	using2 --> using3[\「持ち駒利用中」の状態に/]
```

「持ち駒利用中」の変数を作らずとも、「利用中状態の持ち駒がある」かどうかで持ち駒利用の判定はできたなー。

### 駒の移動範囲

**自分の駒のあるマスを選択したとき、その駒の移動可能範囲のマスを計算し、選択可能状態に変更**します。なお、owner = locActivePlayer であるマスは、自分の駒があるマスなので、移動可能範囲にはなりません。

```js
With({sel: galBoard.Selected},
// 中略
    Switch(sel.piece,
        "ひよこ",
        // ひよこ：前方のマスを選択可能に
        UpdateIf(colBoard,
            owner <> locActivePlayer &&
            (row = sel.row + locActivePlayer && col = sel.col),
            {selectable: true}
        ),
        "にわとり",
        // にわとり：前3マス・左右と、後ろのマスを選択可能に
        UpdateIf(colBoard,
            owner <> locActivePlayer &&
            ((row = sel.row + locActivePlayer || row = sel.row) &&
            (col = sel.col + 1 || col = sel.col || col = sel.col - 1)) ||
            (row = sel.row - locActivePlayer && col = sel.col),
            {selectable: true}
        ),
        "きりん",
        // きりん：前後左右のマスを選択可能に
        UpdateIf(colBoard,
            owner <> locActivePlayer &&
            ((row = sel.row + 1 && col = sel.col) ||
            (row = sel.row - 1 && col = sel.col) ||
            (row = sel.row && col = sel.col + 1) ||
            (row = sel.row && col = sel.col - 1)),
            {selectable: true}
        ),
        "ぞう",
        // ぞう：前後左右ナナメのマスを選択可能に
        UpdateIf(colBoard,
            owner <> locActivePlayer &&
            ((row = sel.row + 1 || row = sel.row - 1) &&
            (col = sel.col + 1 || col = sel.col - 1)),
            {selectable: true}
        ),
        "らいおん",
        // らいおん：周囲のマスを選択可能に
        UpdateIf(colBoard,
            owner <> locActivePlayer &&
            ((row = sel.row + 1 || row = sel.row || row = sel.row - 1) &&
            (col = sel.col + 1 || col = sel.col || col = sel.col - 1)),
            {selectable: true}
        )
    )
// 中略
)
```

もっと短いコードで判定する方法はありそうですが、とりあえず。

### 最終列に到達したときの判定

**プレイヤー -1の場合は 1列目、プレイヤー 1の場合は4列目に達した「ひよこ」を、「にわとり」に変更**します(持ち駒から打った場合はのぞく)。同様に、「らいおん」が最終列に到達したとき、locWinner を設定してゲーム終了にしています。

```js
// ひよこが最終列に到達したとき、にわとりに変更
If(locMovingPiece.piece = "ひよこ" && !locUsingCaptured && ((sel.row = 1 && locActivePlayer = -1) || (sel.row = 4 && locActivePlayer = 1)),
    UpdateIf(colBoard,
        row = sel.row && col = sel.col,
        {piece: "にわとり"}
    )
);
```

最終列の判定は `sel.row = 2.5 + locActivePlayer * 1.5` でまとめられそうですが、わかりにくいのでやめました。

### 手番交替

**プレイヤー番号を 1 と -1 にしたことで、-1 を掛け算すれば切り替え**られます。各マスの owner の値も兼ねており、誰の駒もないときは 0 です。3つの値を取るので、true / false は使いませんでした。

```js
UpdateContext({locActivePlayer: locActivePlayer * -1})

// UpdateContext({locActivePlayer: If(locActivePlayer = 1, 2, 1)})
```

プレイヤー番号を 1 と 2 にした場合は If関数を使うことになりますが、1 と -1 にした方がわかりやすいのではと。

### 画像を反対向きにする

**画像の ImageRotation プロパティ**、初めて使いました。上から下に攻めるプレイヤー 1 の駒は、180度回転させて表示する必要があります。

```js
If(ThisItem.owner = 1,
    ImageRotation.Rotate180,
    ImageRotation.None
)
```

アイコンは Rotation プロパティが数値型なので自由に回転できますが、画像の場合は 90度単位(None, Rotate90, Rotate180, Rotate270) でしか回転できない？

## 気づき

* マスを選択する場面が「動かす駒のマスを選択する」「動かす先のマスを選択する」の 2種類あり、リバーシなどの前者がないゲームよりも処理が複雑になった
* 作りながら設計すると、不要な変数が残ったりしてしまう。大まかな設計をもう少し立ててから作るべきだった
* 無理にコードを短くするより、わかりやすいコードの方がいい
* オリジナル駒を追加したり、初期盤面のエディット機能を追加したりできそう
* 本将棋も作れそう。「どうぶつしょうぎ」にない要素は下記のとおり
    * 「成る」かどうかを選択できるようにする
    * 長距離移動可能な駒(香・角・飛) のロジック
    * 二歩・打ち歩詰めになるときの処理(打てないようにする or 打つ前に戻す or 強制敗北)
    * 移動できない場所に持ち駒を打てないように(どうぶつしょうぎでは、最奥の列に「ひよこ」を打ってもいいらしい)
* アプリを作るのに数時間かかったが、段ボールを切って絵を描いたら 30分くらいで作れそう…(目的は Power Apps の学習！)