ASP.NET Core版 対戦型のマインスイーパーをつくる(2)の続きです。

実はこの記事を書く前にプログラミング実況配信系YouTuberのT.Umezawaさんとその視聴者さんにレビューしていただきました。そこでいくつか改善すべき点を指摘されました。

ここから辛口レビューがはじまります。


(1)旗は右クリックで立てるようにしているが、マウスダウンが押さえた段階で立てて欲しい。
(2)自分で開いたセルとそれ以外のプレイヤーが開いたセルは色を変えるなど視覚的にわかるようにしてほしい。
(3)最後のマスを開いたプレイヤーにはボーナスポイントを与えてはどうか。
(4)マウスをポチポチしなくてもドラッグしたら複数のセルが開くようにしてほしい。
(5)ドラッグの速度が速いと連続したセルがうまく開かない。
(6)旗を立てているセルはクリックしても開かないようにしてほしい。
(7)すでに開いているセルには旗を立てられないようにしてほしい。
(8)終盤になってミスをすると高得点は見込めないので、難しいところは配点を上げるとか、参加人数や残りのセル数で配点を変えてほしい。
(9)クリア時に成績優秀者を表示して欲しい。

などの要望が出されました。

(6)旗を立てているセルはクリックしても開かないようにする。これは旗が立っているにもかかわらず左クリックするあなたが悪い!と言いたいのですが、要望が多かったので対応しました。ただし旗を立ててみたものの間違いに気づいた場合は左クリックするまえに旗をキャンセルする必要があります。

(7)すでに開いているセルには旗を立てられないようにするのも対応しました。

(1)(2)(4)(5)はクライアントサイドの処理で実装します。それから「地雷が置かれていない部分をクリックしたらゲームオーバーになる」のはおかしいのではないかという意見も出されましたが、これはバグではなくそのように見えるだけです。ただ紛らわしい動作なのでこの点も改善します。

(3)と(8)と(9)はしばらくお待ちください。配点はどうやって決めたらいいのかいい案があったら教えてください。

cshtmlファイル

ここではPages\Minesweeperフォルダ内にgame.cshtmlファイルを作成します。そしてgame.cshtmlには以下のように記述します。

Pages\Minesweeper\game.cshtml

そのセルの周囲に埋められている地雷の個数を灰色の背景に色文字でPNGファイルに書いています。自分で開けたセルの場合は背景色と文字色を反転させています(ただし色は白)。

JavaScript部分

それではJavaScriptの部分を示します。

wwwroot\mine-sweeper\app.js

最初にグローバル変数と定数部分を示します。

初期化

初期化の処理を示します。ここではcanvasのサイズと効果音のボリュームを設定しています。そしてAspNetCore.SignalRで接続しようとしています。

接続に成功時の処理

接続に成功したときの処理を示します。サーバーサイドから送られてきたIDをグローバル変数connectionIDに格納します。

ゲームを開始時の処理

[ゲームスタート]のボタンを押したとき、AspNetCore.SignalRで接続されているのであれば、グローバル変数connectionIDは空文字列ではありません。その場合はサーバーサイドにGameStartを送信します。また表示されているスコアを0に戻します。

ゲーム中はゲームスタートできない

サーバーサイドでゲームに参加する処理が正しく行なわれたとき、クライアントサイドではReceiveGameStartedを受信することになります。二重にゲームに参加できないように[ゲームスタート]のボタンを非表示にします(二重にゲーム開始の処理が行なわれないようにサーバーサイドでもチェックをしている)。もっともブラウザを複数起動すれば別のユーザーとみなされ、参加はできますが・・・。

セルの描画

セルを描画する処理を示します。自分で開いたセルと他のプレイヤーが開いたセルの見た目を変えたいので、自分で開けたセルを格納する二次元配列を定義します。そしてInitCellsOpenedYourself関数で初期化をします。

セルを描画する処理を示します。ReceiveDrawを受信したらセルを描画します。

ここでは自分で開けたセルを格納する二次元配列が存在するか確認し、存在しない場合は生成しています。そのあとサーバーサイドから送られてきた文字列で配列をつくり、DrawCell関数に渡しています。

またミス時の描画処理をする関係上、セルの行数と列数、サーバーサイドから送られてきた文字列を保存しています。

DrawCell関数を示します。cellsOpenedYourself[row][col] == trueであれば自分で開いたセルなので描画するイメージを変えています。

セルを開く処理

まずマウスボタンが押されたり離されたとき、マウスが移動したときのイベントリスナーを登録しています。

左ボタンが押された場合はOpenCell関数を呼び出してセルを開く処理をするとともに、ドラッグの始まりかもしれないのでisMouseDwonフラグをtrueにしています。右ボタンが押されたときは旗を立てるための関数を呼び出します。

マウスポインタがcanvasの外に出た場合もマウスドラッグが終了したと見なしています。

左ボタンがおされたときにセルを開く処理をおこなうOpenCell関数を示します。

ブラウザ上でのクリック座標をキャンバス上に変換してサーバーサイドに送ります。マウスがドラッグされているときはOpenCell関数が連続して呼び出されるわけですが、そのとき前回取得したキャンバス上のマウス座標と今回取得された座標を比較します(canvasXとdragLastX、canvasYとdragLastY)。

両者のXY成分がそれぞれCELL_SIZE以上離されている場合は間が抜けていることになるので、この場合はその間の距離を分割して連続したセルがすべて開くように調整します。

旗を立てる処理

旗を立てるときの処理を示します。

マウスが移動しているときの処理です。マウスの左ボタンが押されているときだけ、前述のOpenCell関数を呼びます。

マウスのボタンが離されたときの処理を示します。isMouseDwonフラグをクリアし、dragLastXとdragLastYには不適切な値を設定します。

右クリックしたときのデフォルトの動作(コンテキストメニューの表示)を抑制します。

プレイヤー名とスコアの表示

プレイヤーのスコアや表示内容に変更があった場合、クライアントサイドではReceiveScoresを受信します。この場合は受け取った文字列を各要素のinnerHTMLにセットします。

プレイヤーが地雷ではないセルを開いたときの処理を示します。この場合はReceiveOpenを受信します。効果音を鳴らし、スコアを表示します。また二次元配列cellsOpenedYourselfの要素をtrueにします。

残りのセル数はReceiveUnopenedCellCountで送られてきます。これもページに表示させます。

ステージクリア時の処理

クリアしたときは効果音を鳴らして二次元配列cellsOpenedYourselfを空にします。次のステージで誰かがセルを開けると二次元配列が再構築されます。

ゲームオーバー時の処理

ゲームオーバー時の処理を示します。

ゲームオーバーになるとクライアントサイドではReceiveGameOverを受信することになります。このときは再度[ゲームスタート]のボタンが押せるようにします。それから二次元配列cellsOpenedYourselfを空にすると同時にSetSparks関数を呼び出して周囲に飛び散る火花を作ります。

火花を飛ばす演出

火花を飛ばす演出をいれるのでクライアント側でタイマーを使います。火花はfires配列に格納されますが、通常時は配列は空です。配列内に要素が存在するときはバックアップしておいた配列をつかってセルを描画し、そのうえに火花を描画します。

火花の描画はFireクラスのUpdate関数を呼び出して位置を移動させるとともに描画処理をおこなうことで実現します。1回の更新処理をするたびにFireクラスのTimeToDisappearanceが減少していきます。0になったらfires配列をクリアします。

それから自分が踏んでしまった地雷も描画しないと、クリックした瞬間爆発し、しかもそこには地雷がないのでユーザーはバグではないかと不信に思ってしまいます。そこで踏んでしまった地雷も描画します。この地雷はゲームに再挑戦してどこか別のセルをクリックすると消滅します。