ここ数日間、 Photon オンライン対戦[犬猫将棋」の、 王将がいない場合の勝敗判定を考えていたのですが、実は判定方法って結構単純にできるんです。
「犬猫将棋」では、王将がいないこととが、2ターン目以降、他のプレイヤーにばれると負けになります。
今日はその負けの判定方法を詳しく解説しようと思います。
目次
今までの経緯
突然ですが、Unityを始めてからかれこれ8ヶ月が経ちます。継続的に使っているわけではないので、いまだにC#がよくわからないのです。
しかし、C#に対する知識が浅くとも、UnityとPhotonを使えば、なんとなくゲームらしいものができるようになってきました。
いろいろなところで拾ってきたスクリプトをつなぎ合わせることで、「犬猫将棋」というゲームが奇跡的に成立しています。
至らない点や、間違った解釈もあるかもしれませんが、今後全力で修正していくので、ご容赦ください。
なぜ王将がいない判定が必要なのか
なぜ判定が必要なのか!それは単純です。王がいないことを忘れてしまうからです。
犬猫将棋では、王将を後のほうに打つこともできます。むしろ最初に置くと守らないといけないので危ないです。
ところが、王将を打ちそびれてそのことを忘れ、他の駒をビュンビュンと動かしていると、駒の動きで王将がいないことが証明されてしまう可能性があります。
その場合に知らない間に負けが確定しますので、その勝敗判定をなんとかPCにさせたいということで、考えてみました!
王将がいない判定の基本原理
まず、盤面に王がいるかどうかの判定には、盤面に駒が打ってあるかどうかを判定しないといけません。
要するに、駒には何らかの値を持たせておいて盤面に置かれている場合は特殊な値を取るということが必要です。
それに加え、盤面上の駒が、明らかに王将でない動きをした場合は、その特殊な値を失うという処理も必要になります。
そして、毎ターンその特殊な値を持っている駒の数を集計して、王将がいる可能性が残っていることを確認するということになりそうです。
王将がいない判定の実装の仕方詳細
それでは、判定の詳細を実際のスクリプトを見ながら考えていきましょう。
駒に王ではないフラグを立てる
まず駒にはkoma.csというスクリプトがついているので、駒にflagという名前のbool値を付けてあげました。
falseの場合、王かもしれませんよーという意味です。
public bool flag;//flag スクリプト
手駒はデフォルトで駒台上では全部flagをtrueに設定し、初手の5駒は盤面に打った瞬間にflagをfalseにする仕様にしました。
取った駒は盤上にあっても、flagが常にtrueです。
取った駒が王将である場合、ゲームがすでに終わっていますからね。
盤面の駒(初手の5つ)のみflagがfalseになるようにします。
さらに、盤面の駒が王ができない動きをした場合、flagがtrueになるようにします。
現在のマスの位置を取得する
盤面の駒が、王ができない動きをした場合とは、移動したコマ数が、王の可動範囲を超えた場合です。
そのためには、移動前と移動後の駒の違いを検知する必要があります。
移動前はOnMouseDown()、移動後はOnMouseUp()で現在地のマスを検出するのが良さそうです。
OnMouseDownの中で呼んでいる関数のスクリプトを書いたのが下記になります。
public void TurnCheck() { var komaPosition = new List<int>(); var komaIndex = new List<int>(); var komaNari = new List<bool>(); //var komaFlag = new List<bool>(); koma = GameObject.FindGameObjectsWithTag("koma");//駒タグが付いているものを全部配列として拾う int i; for (i = 0; i < koma.Length; i++) { x = koma[i].transform.position.x; y = koma[i].transform.position.y; KomaModel komaModel = koma[i].GetComponent<KomaModel>(); komaIndex.Add(komaModel.cardIndex);//駒インデックスのリストの作成(のちに使用予定 komaNari.Add(komaModel.naru);//駒が成っているかのリストの作成(のちに使用予定 if (-1.5f > x && x > -2.5f) { if (1.5f < y && y < 2.5f) { masuNum = 1; komaPosition.Add(masuNum); } else if (0.5f < y && y < 1.5f) { masuNum = 6; komaPosition.Add(masuNum); } else if (-0.5f < y && y < 0.5f) { masuNum = 11; komaPosition.Add(masuNum); } else if (-1.5f < y && y < -0.5f) { masuNum = 16; komaPosition.Add(masuNum); } else if (-2.5f < y && y < -1.5f) { masuNum = 21; komaPosition.Add(masuNum); } else { masuNum = 26; komaPosition.Add(masuNum); } } else if (-0.5f > x && x > -1.5f) { if (1.5f < y && y < 2.5f) { masuNum = 2; komaPosition.Add(masuNum); } else if (0.5f < y && y < 1.5f) { masuNum = 7; komaPosition.Add(masuNum); } else if (-0.5f < y && y < 0.5f) { masuNum = 12; komaPosition.Add(masuNum); } else if (-1.5f < y && y < -0.5f) { masuNum = 17; komaPosition.Add(masuNum); } else if (-2.5f < y && y < -1.5f) { masuNum = 22; komaPosition.Add(masuNum); } else { masuNum = 26; komaPosition.Add(masuNum); } } else if (0.5f > x && x > -0.5f) { if (1.5f < y && y < 2.5f) { masuNum = 3; komaPosition.Add(masuNum); } else if (0.5f < y && y < 1.5f) { masuNum = 8; komaPosition.Add(masuNum); } else if (-0.5f < y && y < 0.5f) { masuNum = 13; komaPosition.Add(masuNum); } else if (-1.5f < y && y < -0.5f) { masuNum = 18; komaPosition.Add(masuNum); } else if (-2.5f < y && y < -1.5f) { masuNum = 23; komaPosition.Add(masuNum); } else { masuNum = 26; komaPosition.Add(masuNum); } } else if (1.5f > x && x > 0.5f) { if (1.5f < y && y < 2.5f) { masuNum = 4; komaPosition.Add(masuNum); } else if (0.5f < y && y < 1.5f) { masuNum = 9; komaPosition.Add(masuNum); } else if (-0.5f < y && y < 0.5f) { masuNum = 14; komaPosition.Add(masuNum); } else if (-1.5f < y && y < -0.5f) { masuNum = 19; komaPosition.Add(masuNum); } else if (-2.5f < y && y < -1.5f) { masuNum = 24; komaPosition.Add(masuNum); } else { masuNum = 26; komaPosition.Add(masuNum); } } else if (2.5f > x && x > 1.5f) { if (1.5f < y && y < 2.5f) { masuNum = 5; komaPosition.Add(masuNum); } else if (0.5f < y && y < 1.5f) { masuNum = 10; komaPosition.Add(masuNum); } else if (-0.5f < y && y < 0.5f) { masuNum = 15; komaPosition.Add(masuNum); } else if (-1.5f < y && y < -0.5f) { masuNum = 20; komaPosition.Add(masuNum); } else if (-2.5f < y && y < -1.5f) { masuNum = 25; komaPosition.Add(masuNum); } else { masuNum = 26; komaPosition.Add(masuNum); } } else { masuNum = 26; komaPosition.Add(masuNum); } } } }
ここで、盤面上にない駒はマス値を26として扱っています。
動いた先のマスの位置を取得する
次に、 OnMouseUp() で動いた先のマスを検知します。
検知の仕方はタイミングが違うだけで、動く前のマスの位置を取得する場合と全く一緒の処理です。
スクリプトも一字一句一緒なので、ここでは割愛します。
RoomCustomPropertyで動いたマスを数える
PhotonにはRoomCustomPropertyという、部屋固有の値を保存する機能があります。
移動前、移動後のマスを比べて動いたマスを数えるためには、移動前の値を RoomCustomProperty に記録しておいて、移動後に受信すると便利です。
送信の仕方は下記のようなスクリプトになります。
int j; for (j = 0; j < komaPosition.Count; j++) { var properties = new ExitGames.Client.Photon.Hashtable(); string card = PhotonNetwork.player.ID + "komaPosition" + j; properties.Add(card, komaPosition[j]); PhotonNetwork.room.SetCustomProperties(properties);//駒の現在いるマスの値をルームカスタムプロパティに送る。 }
移動後の値と、 RoomCustomProperty から受信した値の差を計算すれば、動いたマスの数が分かります。
受信と、移動前と移動後のコマが違うかどうかを確かめるスクリプトは以下になります。
if (endkomaPosition[0] != (int)PhotonNetwork.room.customProperties[PhotonNetwork.player.ID + "komaPosition0"])//駒の現在値とルームカスタムプロパティの値を比べる<br> {<br> Debug.Log(endkomaPosition[0]);<br> if ((int)PhotonNetwork.room.customProperties[PhotonNetwork.player.ID + "komaPosition0"] == 26)<br> {<br> komaModel = koma[0].GetComponent();<br> komaModel.flag = false;//フラグを折る<br> }
26から動いた場合は盤面に打たれた場合としてflagをfalseにする判断をしています。
王将が動ける範囲を定義する
王将が5x5のマスの中で動ける範囲とはとても簡単に計算することができます。
図にすると一発です。王の動ける範囲は下の図のようになりますね。
王が動ける範囲は絶対値が1,4,5,6の場合ですので、マス数がそれ以外の場合は王将ではない動きをしたとみなしflagをtrueにします。
盤外から打たれた場合は例外として処理します。
if ((int)PhotonNetwork.room.customProperties[PhotonNetwork.player.ID + "komaPosition0"] != 26)//駒を打ってない場合 { move = Mathf.Abs((int)PhotonNetwork.room.customProperties[PhotonNetwork.player.ID + "komaPosition0"] - endkomaPosition[0]);//移動したマスの絶対値が if (move != 1 && move != 4 && move != 5 && move != 6)//王の動ける範囲でない場合 { komaModel = koma[0].GetComponent<KomaModel>(); komaModel.flag = true;//フラグを立てる }
例えば桂馬のように9とか11とかが出た場合には王の範囲を逸脱しているのがすぐにわかります。
王将が動ける範囲と動いたマスの値を比べる
そして、RoomCustomPropertyを使用して計算した移動前と 移動後のマスの数の差が王将が動ける範囲であるかどうかを比べます。
二つの値を比べ、王将の動ける範囲を逸脱していれば、その盤面の駒はflagがtrue(王ではない)ということになります。
スクリプトをまとめると下記のようになります。
これを11駒分繰り返しています。
if (endkomaPosition[0] != (int)PhotonNetwork.room.customProperties[PhotonNetwork.player.ID + "komaPosition0"])//駒の現在値とルームカスタムプロパティの値を比べる { Debug.Log(endkomaPosition[0]); if ((int)PhotonNetwork.room.customProperties[PhotonNetwork.player.ID + "komaPosition0"] == 26) { komaModel = koma[0].GetComponent<KomaModel>(); komaModel.flag = false;//フラグを折る } if ((int)PhotonNetwork.room.customProperties[PhotonNetwork.player.ID + "komaPosition0"] != 26)//駒を打ってない場合 { move = Mathf.Abs((int)PhotonNetwork.room.customProperties[PhotonNetwork.player.ID + "komaPosition0"] - endkomaPosition[0]);//移動したマスの絶対値が if (move != 1 && move != 4 && move != 5 && move != 6)//王の動ける範囲でない場合 { komaModel = koma[0].GetComponent<KomaModel>(); komaModel.flag = true;//フラグを立てる } } Debug.Log(move); insCore.MakeTurn(0);//値が違う場合ターンを回す }
比べた結果をFindObjectsWithTagで集計する
毎ターンの終了時にFindGameObjectsWithTagで自分の駒を全部取得し、for文を用いてflagがfalseな駒が一つ以上あることを確認します。
koma = GameObject.FindGameObjectsWithTag("koma"); flagNumber = 0; for (i = 0; i < koma.Length; i++) { var newOwner = koma[i].GetComponent<PhotonView>().ownerId;//駒のownerID自駒であることの確認 var komaModel = koma[i].GetComponent<KomaModel>(); if (komaModel.flag == false && PhotonNetwork.player.ID == newOwner) { flagNumber++; } } Debug.Log(flagNumber);//no king check if (flagNumber == 0 && this.turnManager.Turn > 2) { this.ShoumeiText.text = "王将がいないことがしょうめいされました"; this.LoseText.text = "あなたのまけです"; PhotonView.RPC("RPC_WinInfo", PhotonTargets.Others); }
そして、flagがfalseの駒が1つもない場合、自身には負けのダイアログを出します。
そして、RPCを用いて対戦相手には勝ちのダイアログを出します。
[PunRPC] public void RPC_WinInfo() { this.WinText.text = "あなたのかちです"; }
RPCとは「リモート・プロシージャー・コール」と言ってネットワーク対戦の時に双方のプレイヤー又は特定のプレイヤーに処理をさせるメソッドです。
RPCを使用するときは、そのスクリプトにphotonView.csをくっつけて、かつ、Photon.Monobehaviorを継承し、GetComponent<PhotonView>をしておかないとたいていの場合は動かない気がします。
今回の場合は対戦相手だけなのでPhotontargets.Othersを使用します。
例外:禁じ手の場合は別の処理が必要
これで、王将がいない場合の勝敗判定が実装されました。
ただし、禁じ手を行って相手の王将を取った場合、もしくは全滅させた場合は、正しく判定はされません。
例えば1ターン目に飛車を指して、次の自分のターンに飛車を動かして相手の駒を取った場合、本来は禁じ手なので、飛車を動かした側が負けになります。
しかし、プログラム上では双方のプレイヤーのflagのfalseの数が0になってしまうので、両方とも負けと認識されてしまいます。
。これはかなり複雑な話で難しそうなので、おいおい対応していこうと思います。
まとめ
はい!これで王将がいない場合の勝敗判定ができました!
思ったより長い記事になってしまいましたが、割とシンプルに判定ができているのではないかと思います。
今後はいろいろなパターンの勝敗判定を実装して、もれなく勝敗が決まるようにしていきたいと思います。
もしよろしければ、Photonについて私が知りえる限りの情報を徹底的にまとめてみたので、下記の記事も読んで下さいね!