ASP.NET coreで対戦型『殺しの七並べ』をつくる(1)の続きです。

今回はゲーム全体を管理するGameクラスを定義します。ゲームは対戦者以外の人も観戦できるようにします。

以降は名前空間は省略して記述します。

プロパティ

プロパティを示します。

ユーザーの誰かがエントリーボタンをクリックしたらゲームが開始されます。しかしそれ以外のユーザーが参加するかもしれないので、10秒間待機してから開始します。ゲームが開始されて以降もユーザーはゲームに参加することができます。PLAYER_MAXに足りない部分はコンピュータがプレイヤーとなってゲームを進行させます。

コンストラクタ

Gameオブジェクトが存在しないときにユーザーの誰かがエントリーボタンをクリックしたらGameオブジェクトが生成されます。ここではコンストラクタでおこなわれる処理を示します。

まず手番があるプレイヤーですが、添え字が0のプレイヤーです。SECONDS_TO_START_MAX秒後にゲームがはじまるのでSecondsToStartプロパティに値をセットします。プレイヤーが出したカードと殺されたカードはゲームが開始されるまえは存在しないのでCardsとDeadCardsには空のリストをセットします。

Playersプロパティに要素数PLAYER_MAXの配列をセットして、そのなかにPlayerオブジェクトを格納します。格納されるオブジェクトは最初はすべてNPCです。人間のユーザーがゲームに参加するときはこれといれかえます。またユーザーがゲームから離脱したらその穴はNPCで埋めます。

プレイヤーの追加と削除

このゲームではユーザーはゲーム開始時でなくてもいつでもプレイに参加し、離脱することができます。なのでGameオブジェクトのなかに新しいプレイヤーを追加したり、離脱されたときの処理が必要です。プレイヤーを追加したり削除する処理を示します。

ゲームに参加しようとしているユーザーはすでに ASP.NET SignalRで使われる接続の一意のID(以下、connectionID)を持っています。これをつかってどのユーザーがどのオブジェクトに対応するのかを管理します。

プレイヤーを追加するときはユーザーが希望するプレイヤー名とconnectionIDを引数とするAddPlayerメソッドが呼び出されます。このときすでに同じconnectionIDをもつプレイヤーがいる場合は二重参加になるので参加を拒否します。それ以外のときはPlayersのなかにNPCがいるか調べます。もし見つかったらそれをプレイヤーとして割り当てます。複数みつかった場合は乱数でそのどれかを割り当てます。

プレイヤーの割り当てはPlayerオブジェクトのConnectionIDプロパティにconnectionIDを、Nameプロパティにプレイヤー名をセットすることでおこなわれます。NPCとプレイを交代するという感じです、

NPCが存在しない場合はユーザーはゲームに参加することはできません。

Playerオブジェクトが割り当てられたときはその添え字を戻り値として返します。処理が拒否されたときやNPCが存在しなかった場合は-1を返します。

ユーザーがゲームから離脱するときはconnectionIDを引数とするRemovePlayerメソッドが呼び出されます。この場合はそのユーザーに対応するPlayerオブジェクトをNPCに変更します。新しいNPCの名前を生成してNameプロパティにセットし、ConnectionIDプロパティは空文字列をセットします。

ゲーム開始の処理

ゲームが開始されたらまずカードを各プレイヤーに配布する処理をおこないます。そのためにCardオブジェクトを生成してシャッフルする処理をおこないます。

Cardオブジェクトを生成する処理を示します。カードのスート(スペードやハートのマーク)は0~3の整数で表わし、番号はそのまま1~13を使います。CreateCardsメソッドでは二重ループで52枚のカードを生成しています。

CreateCardsメソッドで生成した52枚のカードは数が連続しているのでシャッフルの処理をおこないます。それがShuffleCardsメソッドです。乱数を生成してCardオブジェクトのフィールド変数Randomに格納します。そしてこの値の順にオブジェクトをソートします。これでシャッフルの処理はおこなわれています。

カードのシャッフルが終わったら各プレイヤーに配布します。普通の7並べではカードを配布してそのあと7をもっている人はそのカードを出してから開始されるのですが、ここではプレイヤーのカードが多くならないように(プレイヤーが持っているカードを表示させるとき面倒くさいという作り手の事情)さきに7のカードを取り除いて残りのカードを配布します。

カードを配布はPlayers[プレイヤー番号].CardsにCardオブジェクトを追加する方式でおこないます。すべて配布したら番号でソートします。

ゲーム終了のチェックと手番の変更

着手が終わったら次のプレイヤーに手番を移します。手番はCurPlayerNumberプロパティの値でわかります。Players[CurPlayerNumber]が手番のプレイヤーに該当するオブジェクトです。

まずゲームは終了している場合は処理は不要です。そうでない場合はCurPlayerNumberをインクリメントしてPlayers.Lengthで割ったときの剰余をセットします。このプレイヤーはすでに上がっているかもしれないのでそれを確認します。もしnextPlayerが上がっていないプレイヤーである場合はこのプレイヤーの持ち時間をSECONDS_TO_HAND_MAXにして処理を終了します。

もしそのプレイヤーが上がっている場合は順位が暫定順位(手持ちのカードがなくなった順位)が確定しているかを調べます。暫定順位が確定している場合は暫定順位を確定させます。すべてのプレイヤーのPlayer.Rankingの最大値が-1のときは誰も上がっていないということなので1位に、それ以外の時は最大値に1を追加したものとセットします。そのあとゲーム終了が確定するか、カードを持っているプレイヤーが見つかるまでループを回します。

手番の変更

ゲーム終了のチェック

ゲームは終了しているかを調べる処理を示します。

最初は各プレイヤーの暫定順位(手持ちのカードがなくなった順位)には-1がセットされていて、カードがなくなったときに暫定順位が確定します。

実際の順位は1以上なので、すべてのPlayerオブジェクトのRankingプロパティが0より大きな値であればすべてのプレイヤーの暫定順位が確定していることになります。その場合はIsFinishedフラグをtrueにします。またプレイヤーの本当の順位を確定させてFinishedイベントを送信します。

プレイヤーの本当の順位は殺されたカードが少ないほうが上位、同じ場合はさきに上がったほうが上位です。

イベントハンドラの引数で使われるFinishedArgsクラスの定義を示します。

着手によってカードは殺されたか?

カードを出すことで別のカードが殺されたかどうかを調べる処理を示します。

殺すことができるカードは出されたカードの上下左右とこれとつながっているカードです。そこでカードが置かれたら、その上下左右にあるカードとつながっているカード群を取得します。このとき同じカードを何度も調べて無限ループにならないように一度チェックしたカードは記憶しておきます。

出されたカードの上下左右それぞれのカードとつながっているカード群を取得したら、それらのSuitとNumberの最大値と最小値を調べます。もし(Numberの最大値 – Numberの最小値 + 1) * (Suitの最大値 – Suitの最小値 + 1) が取得されたカードの個数と一致したらカードは四角く囲われていることになります。この場合、CheckKillメソッドはそのカードを出したPlayerリストを返します。

プレイヤーの着手時の処理

プレイヤーがカードを出そうとしたときの処理を示します。

まずカードを出すことができるかを調べる処理を示します。出せるカードとはすでに出されているカードと縦横斜めのどれかでつながっているカードです。

ユーザーであるプレイヤーがカードを出す処理をするときは、そのプレイヤーはゲームに参加しているのかを調べます。そのつぎに出されようとしているカードはプレイヤーによって所持されているのかを調べます。さらに出そうとしているカードは出せるカードなのかを調べます。

出せるカードを出そうとしている場合は、そのCardオブジェクトをPlayer.CardsからGame.Cardsに移動させます。そしてカードが出されたことを示すPutCardイベントの送信します。またこれによって他のカードが殺されたのであればそれを示すKilledイベントの送信をおこないます。そして手番をつぎのプレイヤーに移します。このときにフィールド変数_ignoreUpdateをtrueにして、つぎの更新時はなにも処理をおこなわない(NPCが着手しない)ようにします。

カードを出すことでプレイヤーの手持ちのカードはすべてなくなってしまったかもしれません。この場合は暫定順位をつけます。また出せないカードを出そうとした場合はDenyCardイベントを送信します。

更新処理

アクションゲームとちがってプレイヤーがカードを出さない限り更新処理は必要ないので1秒おきに更新処理をおこないます。

大きくわけて更新時の状態は3つあります。それはゲームが開始される前、ユーザーに手番がある場合、NPCに手番がある場合です。ゲームが終了したら更新処理はおこないません。

NPCに手番がある場合はすぐに次の手を探して着手させますが、ユーザーに次の手番がある場合、着手のタイミングはわかりません。持ち時間があるので1秒ごとにカウントダウンをします。また更新処理は1秒ごとにおこなわれますが、ユーザーの着手のタイミングと重なるとユーザーの着手後すぐにNPCの着手がおこなわれるため、ユーザーの着手がおこなわれたら次の更新処理は1回だけなにもしないようにします(フィールド変数_ignoreUpdateはこのときに必要)。

ゲーム開始前の更新処理

ゲーム開始前の更新処理を示します。

SecondsToStartプロパティが0より大きい場合はデクリメントします。もしSecondsToStartプロパティが0のときはCardの生成、シャッフル、配布の処理をおこないます。そしてフィールド変数IsStartedをtrueに変更します。

手番があるプレイヤーをPlayers[0]にして持ち時間をSECONDS_TO_HAND_MAXにします。そのあとStartedイベントを送信します。

NPCに手番があるときの更新処理

NPCに手番があるときの更新処理を示します。この場合はNPCに着手させます。

NPCが所持するカードが存在する場合は出せるカードがあるか調べます。複数見つかった場合はそのなかからランダムに選んでそれを出します。着手ができた場合もそうでない場合も手番を次のプレイヤーに移します。

プレイヤーに手番があるときの更新処理

ユーザーであるプレイヤーに手番があるときの更新処理を示します。この場合はそのプレイヤーの持ち時間を減らします。持ち時間が0になったら強制的にパス扱いになります。強制パスになった場合はPlayerPassedイベントを送信します。