ASP.NET Core と ASP.NET SignalR でオンライン対戦できるオセロをつくります。「オセロ」「Othello」という名称は株式会社オセロの登録商標であり、メガハウスが専用使用権を有しています。以降はリバーシ(Reversi)と呼ぶことにします。

対戦者のペアができれば同時に複数の対戦ができるものとし、対戦者以外のユーザーは現在行われている対戦や過去の対戦を観戦できるようなものをつくります。

着手しないといつまで経っても終わらないので1手20秒という制限時間を設けます。残り5秒を切ったら着手候補点を表示させます。

クライアントサイドへのデータの送信は1秒間に1回とし、そんなに頻繁にデータを送信するわけではないので json で送ることにします。送信するのは、対局の識別ID、盤上の状態、対局者の名前、どちらの手番か、残り時間などです。

ではさっそくつくっていきましょう。名前空間はReversiとします。

Playerクラスの定義

コンストラクタの引数 id はASP.NET SignalRで使われる一意の接続IDです。playerNameはユーザーが自由につけることができる名前ですが、< や > があると HTML上ではうまく表示されなくなるのでここでエスケープ処理をしています。また接続IDはセキュリティ上、クライアントサイドに送信しないほうがよいので[JsonIgnore]属性をつけて json への変換対象から除外しています。

Historyクラスの定義

着手の履歴を閲覧できるようにHistoryクラスとDataクラスを定義します。

Gameクラスの定義

Gameクラスを定義します。

以降は名前空間の部分を省略します。

プロパティとコンストラクタ

プロパティとコンストラクタを示します。

オブジェクトに通し番号をつけ、これと現在の時刻を組み合わせて一意のIDを生成します。

盤上の状態は8個の長さ8の文字列で表します。Nはなにもない部分、Bは黒石、Wは白石、bは黒の手番のときに着手可能な部分、wなら白の手番のときに着手可能な部分です。

コンストラクタを示します。プロパティに初期値を設定しています。

ReverseメソッドはStones[row][col]をchageToに変更します。

イベントの定義

イベントを定義します。

SuccessfulEntryイベントはユーザーによるエントリーが成功したときに送信されます。

Matchedイベントは対戦を開始するにあたってふたりの対局者が決まったときに送信されます。

StatusChangedイベントは盤上の状態や残り時間の変動がおきたときに送信されます。

Denyイベントはユーザーが着手不能な場所に着手しようとしたときに送信されます。

対局開始までの処理

対戦を希望するユーザーはページ上のエントリーボタンを押下して相手がエントリーしてくるのを待ちます。

IsMatchedメソッドはマッチングが成立しているかどうかを返します。GetWaitingPlayerメソッドはマッチングが成立していないオブジェクトであって対戦希望者が片方だけ決まっているときにそのPlayerオブジェクトを返します。

ユーザーがエントリーボタンを押下したらマッチングが成立していないGameオブジェクトがあるか探します。もしあればそのオブジェクトのSetPlayerメソッドが呼び出されてマッチングが成立します。なければ新しいGameオブジェクトが生成されたあとそのSetPlayerメソッドが呼び出され、対戦相手を待つことになります。

対局開始時におこなわれる処理を示します。

まずどちらが先手後手になるのかを決めます(50%の確率でPlayersを入れ替えるだけ)。そのあとマッチング成立のイベントを送信して1秒待機後に対局を開始します。

対局が開始されたら、手番を黒にして残り時間の減算を開始します。盤上の状態をStatusChangedイベントで送信します。そのあとHistoriesに現在の盤上の状態を格納します。また黒の着手可能点を Stonesプロパティ内に記録します(後述)。

着手可能点を調べる

GetReversibleStonesメソッドは(row, col)に着手したときにひっくり返される石がある位置のリストを取得します。これが返すリストが空でなければ着手可能であることになります。

CanPutStoneメソッドは着手可能な場所があるか(強制パスにならないか)を調べます。

CheckPossiblePositionsメソッドはStonesプロパティの着手可能点に相当する文字を ‘b’ または ‘w’ に置き換えます。

ClearPossiblePositionsメソッドはStonesプロパティに書き込まれていた ‘b’ や ‘w’ を ‘N’ に戻します。

着手と終局判定の処理

着手と終局判定の処理を示します。

BlackOrWhiteメソッドは引数としてASP.NET SignalRで使われる一意の接続IDが渡されたときにそれが先手なのか後手なのかを示す文字を返します。

IsFinishメソッドは終局しているかどうかを調べます。すべてのマスが石で埋まっていたり、双方が着手不能であれば終局です。

Jugdeメソッドは終局時にそれぞれの石の数を数えます。

PutStoneAsyncメソッドはユーザーが着手可能な場所に着手しようとしたときに着手したあと、0.5秒後に挟まれている石をひっくり返します。着手時に一時的にIsAcceptedプロパティをtrueにして着手が受理されたことをStatusChangedイベントで送信します。同時にStonesプロパティに書き込まれていた ‘b’ または ‘w’ を ‘N’ に戻し、減算されていた残り時間を最大値に戻します。また盤面の履歴を保存します。

そのあと終局判定をします。終局していない場合は相手番が着手可能であれば手番を変更し、そうでない場合は強制パスとしてひきつづき着手したプレイヤーの手番とします。強制パスの場合はユーザーにわかるようにIsPassForcedフラグをtrueにしてからStatusChangedイベントを送信します。

終局している場合は、IsFinishedフラグをtrueにし、どちらがどれだけ勝ったのかがわかるようにしてからStatusChangedイベントを送信します。そのあと履歴をファイルに保存し、すべてのユーザーが閲覧できるようにします。

ユーザーが着手しようとしたとき PutStoneメソッドが呼び出されます。

引数として渡されたASP.NET SignalRで使われる一意の接続IDをもつプレイヤーが対局者であり、自分の手番であり、引数で指定された位置が着手可能である場合のみ上記の着手の処理がおこなわれます。自分の手番でなかったり、着手不能な場所に着手しようとしたときはDenyイベントが着手できない理由とともに送信されます。

残り時間を減算させる処理を示します。

自分の手番で残り時間が0になったら時間切れで負けです。この場合は時間切れにならなかったプレイヤーが勝者であり、対局履歴をファイルに保存します。そうでない場合は残り時間を減算します。どちらの場合もStatusChangedイベントを送信してクライアントサイドに状態の変化があったことを知らせます。

対局履歴の読み出しと保存

対局履歴の読み出しの処理を示します。FilePathに保存されているjsonファイルを読み出してDataのリストに変換します。

保存する処理を示します。

FilePathに保存されているjsonファイルを読み出したあと新しい対局データを追加します。そして新しい順にソートして先頭100件のみを保存します。

GetJsonPastMatchesメソッドはjsonファイルから読み出した文字列を返します。

観戦者の追加と削除

現在おこなわれている対局を観戦しているユーザーを追加したり削除する処理を示します。ここでやっているのはWatcherIDsプロパティに対し、引数として渡されたASP.NET SignalRで使われる一意の接続IDを追加したり削除しているだけです。