ASP.NET Core版 対人対戦できるぷよぷよをつくる(6)の続きです。前回はログイン機能を実装したので、今回はこれをつかって、ログインしてプレイしたユーザーの記録を残すことができるようにします。
Contents
ログイン用のページをつくる
まずログイン用のページをつくります。プロジェクトのPagesフォルダの配下であればどこでもよいのですが、名前空間とフォルダ名が一致しているとちょっと困った問題がおきます。そこでPages\Puyo-MatchフォルダのなかにLogin.cshtmlをつくります。そして以下のように記述します。
やることはログイン機能追加の準備 ASP.NET Core版 対人対戦できるぷよぷよをつくる(6)とほとんど同じです。ログインしていないときには[ログイン]のボタンが、ログインしているときは[ログアウト]のボタンが表示されます。ボタンを2つつくるのではなく、同じボタンに表示されるテキストを変えているだけです。
またここにはどのような処理をするのか書いていませんが、ログインまたはログアウトに成功したらゲーム用のページにリダイレクトさせます。失敗したときはページの遷移はせずに失敗した旨を表示させます。
Pages\Puyo-Match\Login.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
@page @using PuyoMatch @model LoginModel @{ Layout = null; string loginPlayerName = Model.GetLoginPlayerName(); string loginResultText; string loginButtonText; string displayRegisterForm; string displayLoginTextBox; if (loginPlayerName == "") { loginResultText = "ログインしていません"; loginButtonText = "ログイン"; displayRegisterForm = "block"; displayLoginTextBox = "inline-block"; } else { loginResultText = $"[{loginPlayerName}] でログイン中"; loginButtonText = "ログアウト"; displayRegisterForm = "none"; displayLoginTextBox = "none"; } if (Model.LoginResultText != "") loginResultText = Model.LoginResultText; string registerResultText = Model.RegisterResultText; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <title>ログイン・ユーザー登録</title> <link rel="apple-touch-icon" href="https://lets-csharp.com/wp-content/themes/cool_black/images/apple-touch-icon.png"> <link rel="apple-touch-icon-precomposed" href="https://lets-csharp.com/wp-content/themes/cool_black/images/apple-touch-icon.png"> <link rel="icon" href="https://lets-csharp.com/wp-content/themes/cool_black/images/apple-touch-icon.png"> <link rel="shortcut icon" type="image/x-icon" href="https://lets-csharp.com/wp-content/themes/cool_black/favicon.ico"> <style> #container { width:700px; margin:20px; line-height:200%; } .b { font-weight:bold; } </style> </head> <body> <div id="container"> <p class = "b">ログイン用フォーム</p> <form method="post"> <div style="display:@displayLoginTextBox"> ユーザー名:<input type="text" asp-for="InputPlayerName" /><br> パスワード:<input type="password" asp-for="Password" /> </div> <input type="submit" formaction="./Login?handler=Login" value="@loginButtonText" /><br> </form> <p>@loginResultText</p> <div style="display:@displayRegisterForm"> <p class="b">登録用フォーム</p> <form method="post"> 希望するユーザー名:<input type="text" asp-for="InputRegisterName" /> <input type="submit" formaction="./Login?handler=Register" value="登録" /><br> パスワード:<input type="password" asp-for="Password1" maxlength="32" /><br> パスワード(2回):<input type="password" asp-for="Password2" maxlength="32" /><br> </form> <p>@registerResultText</p> </div> </div><!-- /#container --> </body> </html> |
LoginModelクラスの定義
次に実際の処理をするコードを書きます。
ハッシュ値を生成するクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace PuyoMatch { public static class Hash { static System.Security.Cryptography.HashAlgorithm hashAlgorithm = System.Security.Cryptography.SHA256.Create(); public static string GetHashValue(string str) { return string.Join("", hashAlgorithm.ComputeHash(System.Text.Encoding.UTF8.GetBytes(str)).Select(x => $"{x:x2}")); } } } |
次にユーザー登録、ログイン、ログアウトの処理をするLoginModelクラスを定義します。インデントが深くなるので名前空間は省略します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Security.Claims; using System.Text; namespace PuyoMatch { public class LoginModel : PageModel { } } |
1 2 3 |
public class LoginModel : PageModel { } |
ログインユーザー名の取得
GetLoginPlayerNameメソッドはログインしている場合はユーザー名を返し、そうでない場合は空文字列を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class LoginModel : PageModel { public string GetLoginPlayerName() { if (User.IsInRole("Player")) { List<Claim> list = User.Claims.ToList(); Claim? ret = list.FirstOrDefault(_ => _.Type == "PlayerName"); if (ret != null) return ret.Value; } return ""; } } |
ユーザー登録されているか?
IsRegisteredメソッドはすでにユーザー登録されているかどうかを調べます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class LoginModel : PageModel { public const string ResistPath = "../registered-data-puyomatch.dat"; // ユーザー名とパスワードの両方が正しいか? bool IsRegistered(string playerName, string password) { bool isRegistered = false; if (System.IO.File.Exists(ResistPath)) { StreamReader sr = new StreamReader(ResistPath, Encoding.UTF8); while (true) { string? str = sr.ReadLine(); if (str == null) break; string[] strings = str.Split('\t'); if (strings[0] == playerName) { if (password == "") isRegistered = true; if (password != "" && strings.Length > 1 && strings[1] == Hash.GetHashValue(password)) isRegistered = true; break; } } sr.Close(); } return isRegistered; } // ユーザー名はすでに登録されているか? bool IsRegistered(string playerName) { return IsRegistered(playerName, ""); } } |
ユーザー登録の処理
ユーザー登録するための処理を示します。
フォームに希望するユーザー名とパスワードを入力して送信したときに、すでに同じユーザー名で登録されていないか、2回入力したパスワードは同じかをチェックして問題がなければユーザー登録の処理をおこないます。
‘や”やタブ文字があると不具合を起こすので、最初からこれらの文字列があると登録できないようにしています。登録された内容がサーバー上にあるファイルに保存されますが、このときユーザー名とハッシュ化されたパスワードだけでなく、登録時刻をハッシュ化した文字列と勝ち数、ゲームの総得点(登録したときはいずれも0)も文字列に変換して保存しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public class LoginModel : PageModel { string _password1 = ""; [BindProperty] public string Password1 { set { _password1 = value; } get { return _password1; } } string _password2 = ""; [BindProperty] public string Password2 { set { _password2 = value; } get { return _password2; } } string _inputRegisterName = ""; [BindProperty] public string InputRegisterName { set { _inputRegisterName = value; } get { return _inputRegisterName; } } public string RegisterResultText = ""; public void OnPostRegister() { if (InputRegisterName == null || Password1 == null || Password1 != Password2) { RegisterResultText = "入力された文字列が不正です。"; return; } if (InputRegisterName.IndexOf("'") != -1 || InputRegisterName.IndexOf("\"") != -1 || InputRegisterName.IndexOf("\t") != -1) { RegisterResultText = "シングルクォーテーション(')、ダブルクォーテーション(\")、タブ文字は使用不可です。"; return; } if (IsRegistered(InputRegisterName)) { RegisterResultText = "すでに登録されています。ほかの文字列を指定してください。"; } else { try { StreamWriter sw = new StreamWriter(ResistPath, true, Encoding.UTF8); sw.WriteLine($"{InputRegisterName}\t{Hash.GetHashValue(Password1)}\t{Hash.GetHashValue(DateTime.Now.Ticks.ToString())}\t{0}\t{0}"); sw.Close(); RegisterResultText = "登録が完了しました。"; } catch { RegisterResultText = "登録に失敗しました。もう一度登録作業を繰り返してください。"; } } } } |
ログイン処理
ログインの処理をおこなう処理を示します。ログインに成功したらゲームのページにリダイレクトさせています。正しくないユーザー名やパスワードを入力した場合はページの遷移はおこなわずに入力内容が正しくない旨を表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
public class LoginModel : PageModel { string _inputPlayerName = ""; [BindProperty] public string InputPlayerName { set { _inputPlayerName = value; } get { return _inputPlayerName; } } string _password = ""; [BindProperty] public string Password { set { _password = value; } get { return _password; } } public string LoginResultText = ""; public string LoginResultText = ""; public async Task<RedirectResult?> OnPostLogin() { // ログインしていないなら if (!User.IsInRole("Player")) { if (InputPlayerName == null || Password == null) { LoginResultText = "名前を入力してください"; return null; } if (IsRegistered(InputPlayerName, Password)) { List<Claim> claims = new List<Claim> { new Claim("PlayerName", InputPlayerName), new Claim(ClaimTypes.Role, "Player"), }; var claimsIdentity = new ClaimsIdentity( claims, CookieAuthenticationDefaults.AuthenticationScheme); var authProperties = new AuthenticationProperties { IsPersistent = true }; await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties ); } else { LoginResultText = "登録されていないかパスワードが不正です"; return null; } } else { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } // ゲームのページへ return Redirect("./game"); } } |
偽装防止策
GetPlayerTokenメソッドはユーザー名したときの時刻をハッシュ化した文字列を返します。これはページにアクセスしたときにログインされているのであれば取得されます。これがあるかどうか、これがユーザー登録しているユーザーのものと一致するかどうかでなりすましを防止しようというものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class LoginModel : PageModel { public static string GetPlayerToken(string playerName) { string ret = ""; if (System.IO.File.Exists(ResistPath)) { StreamReader sr = new StreamReader(ResistPath, Encoding.UTF8); while (true) { string? str = sr.ReadLine(); if (str == null) break; string[] strings = str.Split('\t'); if (strings[0] == playerName) { if (strings.Length > 2) ret = strings[2]; break; } } sr.Close(); } return ret; } } |
ゲーム用ページのcshtmlの修正
次にゲームのページのcshtmlを示します。変更点はログインボタンを追加しただけです。
ログインしている場合はユーザー名を表示させログアウト用のボタンを表示します。そうでない場合はログイン用のボタンを表示させます。またログインしている場合はハンドルネームはログインユーザー名をそのまま使うのでハンドルネーム設定用のテキストボックスを非表示にしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
@page @model PuyoMatch.LoginModel @{ Layout = ""; string baseurl = アプリを公開したいurl; string loginPlayerName = Model.GetLoginPlayerName(); string playerToken = ""; if (loginPlayerName != "") playerToken = PuyoMatch.LoginModel.GetPlayerToken(loginPlayerName); string loginResultText; string loginButtonText; string displayPlayerNameInput; if (loginPlayerName == "") { loginResultText = "ログインしていません"; loginButtonText = "ログイン"; } else { loginResultText = $"[{loginPlayerName}] でログイン中"; loginButtonText = "ログアウト"; } if (loginPlayerName == "") displayPlayerNameInput = "inline-block"; else displayPlayerNameInput = "none"; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>鳩でもわかるオンライン対戦型「ぴよぴよ」Ver 1.0 - 鳩でもわかるASP.NET Core</title> <style> body { background-color: #000; color: #fff; font-family: "MS ゴシック"; } #container { width: 700px; } .display-none { display: none; } </style> </head> <body> <div id="container"> <div class="display-none"> @for (int i = 0; i < 5; i++) { @for (int k = 0; k < 20; k++) { if (k < 10){ <img src="@(baseurl)/puyo-match/puyo-images/@(i+1)-0@(k).png" alt="" id="type@(i+1)-0@(k)" /> } else { <img src="@(baseurl)/puyo-match/puyo-images/@(i+1)-@(k).png" alt="" id="type@(i+1)-@(k)" /> } } } <img src="@baseurl/puyo-match/puyo-images/wall.png" alt="" id="wall" /> </div> <!-- /.display-none --> <div style="margin-left:60px; color:#ff0000" id="how-to"> <p> [エントリーする]ボタンを押すとすでにエントリーしている人がいると対戦が始まります。 誰もエントリーしていないと誰かがエントリーした段階で対戦が始まります。 このページから別のページへ移動すると再度エントリーが必要です。 </p> </div> <div style="margin-left:60px; color:#ff0000" id="errer"> <p>エラー:通信が切れました。</p> </div> <div style="margin-left:60px"> <p id="notify"></p> </div> <canvas id="can"></canvas> <br> <div style="margin-left:60px;"> <!-- ログインしているときはユーザー名をここに表示させる --> <!-- ログインまたはログアウト用のボタンを表示させる --> @loginResultText <input type="button" onclick="location.href='./Login'" value="@loginButtonText"><br> <input type="checkbox" id="sound-checkbox"><label for="sound-checkbox">音を出す</label> <div id="entry"> <div style="display:@displayPlayerNameInput"> <!-- ログインしているときはこの部分は必要ないので非表示とする --> <label for="player-name" id="player-name-label">ハンドルネーム</label> <input type="text" id="player-name" maxlength='16' /> </div> <input type="button" id="enteryButton" value="エントリーする" onclick="Entery('@loginPlayerName')"> <input type="button" onclick="location.href='./games'" value="他の対戦を観戦する"> <!-- ログインユーザーの成績をランキングページでみることができるようにした --> <input type="button" id="showTop30Button1" onclick="location.href='./ranking'" value="ランキングをみる"> </div> <p id="notify1">あなた以外に <span id="player-count"></span> 人がエントリーしています。<br> 現在 <span id="game-count"></span> の対戦がおこなわれています。</p> <p>遊び方</p> <p> 左右の移動 LeftKey RightKey <span style="margin-left:20px;">落下 DownKey</span><br> 左回転 Z 右回転 X (回転2度押しでクイックターン:縦に並んでいる組ぷよを180度回せる) </p> <p id="conect-result"></p> </div> </div><!-- / #container --> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> <script> let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/PuyoMatchHub").build(); let base_url = '@baseurl'; let loginPlayerName = '@loginPlayerName'; // ログインしているプレイヤー名 let playerToken = '@playerToken'; // 偽装防止のための文字列 </script> <script src="@baseurl/puyo-match/puyo-match.js"></script> </body> </html> |
JavaScript部分の修正
JavaScript部分を修正します。エントリーボタンがクリックされたときにサーバーサイドにplayerTokenもいっしょに送信します。変更部分はここだけです。
wwwroot\puyo-match\puyo-match.js
1 2 3 4 5 6 7 8 9 10 11 |
function Entery(player_name) { if (connectionID != '') { let playerName = document.getElementById('player-name').value; if (player_name != '') playerName = player_name; connection.invoke("Entery", connectionID, playerName, playerToken).catch(function (err) { return console.error(err.toString()); }); } } |
既存のクラスの修正
ゲームのページにアクセスするとログインしているならユーザーネームがloginPlayerNameに格納されます。ただしこれだけだとクライアントサイドで書き換えることができるので、偽装防止のための文字列も同時にサーバーサイドから取得してplayerTokenに保存しておきます。ゲームを開始するときに両方がそろっていればログインユーザーであると見なすことができます。
またランキングをアップさせるために自分と自分を対戦してすぐに負けて勝ち点を増やすという不正も考えられます。そこでログインしている場合はplayerTokenに同じ文字列が格納されることを利用して自作自演を防ごうとしているのですが、これは確実な方法ではありません。ブラウザを変えれば別人として認識されるし、ログインしてエントリーしたあと、ログアウトしてエントリーすれば自分同士で対戦できてしまいます。
残念ながら自演を100%見破ることはできません。IPアドレスを使う方法もありますが、PCとスマホの両方を使われてしまえば不正はやりたい放題です。ただ高スコアを取れば賞品が出るわけでもないし、そこまでする暇な人はいるのでしょうか?
PlayerInfoクラスの修正
まずプレイヤーの情報を管理するPlayerInfoクラスを修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
namespace PuyoMatch { public class PlayerInfo { // コンストラクタの変数が増えた public PlayerInfo(string CconnectionId, string playerName, string playerToken) { ConnectionId = CconnectionId; PlayerName = playerName; if (playerToken != "" && GetPlayerToken(playerName) == playerToken) { // コンストラクタの引数playerTokenが空文字列ではなく // GetPlayerToken(playerName)と一致するならログインユーザーと判断する IsLoginUser = true; PlayerToken = playerToken; } else { IsLoginUser = false; PlayerToken = ""; } } public string GetPlayerToken(string playerName) { return LoginModel.GetPlayerToken(playerName); } // 自作自演を防止? public void CheckSelfProduced(PlayerInfo playerInfo) { // トークンが存在しないならログインユーザーではない if (playerInfo.PlayerToken == "") playerInfo.IsLoginUser = false; if (PlayerToken == "") IsLoginUser = false; if (PlayerToken != "" && playerInfo.PlayerToken != "" && playerInfo.PlayerToken == PlayerToken) { // 同じなら同一プレイヤーの自作自演となるが、完全に防ぐことはできない IsLoginUser = false; playerInfo.IsLoginUser = false; } } // プロパティは変更なし public string ConnectionId { get; } public string PlayerName { get; } public string EscapedPlayerName { get { return PlayerName.Replace("<", "<").Replace(">", ">"); } } public bool IsLoginUser { private set; get; } public string PlayerToken { get; } } } |
PuyoMatchGameクラスの修正
ここで追加しているプロパティは対戦している両者がログインユーザーなのかどうかを示すものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace PuyoMatch { public class PuyoMatchGame { // 追加されたプロパティ public bool[] IsLoginUsers { get; } public PuyoMatchGame(PlayerInfo[] playerInfos) { // これまでのものに加えて以下を追加する IsLoginUsers = new bool[2]; IsLoginUsers[0] = playerInfos[0].IsLoginUser; IsLoginUsers[1] = playerInfos[1].IsLoginUser; } } } |
PuyoMatchHubクラスの修正
PlayerInfoクラスのコンストラクタの引数が増えているのでエントリーしたときの処理が変更になります。PuyoMatchHubクラスもインデントが深くなるので名前空間 PuyoMatchは省略して書きます。
Enteryメソッドが長くなったので分けました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class PuyoMatchHub : Hub { public async void Entery(string id, string playerName, string playerToken) { if (!ClientProxyMap.ContainsKey(id)) return; if (playerName == "") { await ClientProxyMap[id].SendAsync("NotifyToClient", "名前は必ず登録してください"); return; } if (WaitingPlayers.ContainsKey(id)) { await ClientProxyMap[id].SendAsync("NotifyToClient", "すでに登録されています"); return; } PlayerInfo player = new PlayerInfo(id, playerName, playerToken); // 登録したユーザーに登録処理がおこなわれたことを通知する if (ClientProxyMap.ContainsKey(id)) await ClientProxyMap[id].SendAsync("EnteredToClient", $"[ {player.EscapedPlayerName} ]でエントリーしました。"); // 自分以外に待機している人がいたらその人と対戦する if (WaitingPlayers.Count > 0) await OnMatchingEstablished(player); else await OnMatchingFailed(player); } } |
エントリーしたときに誰かがすでにエントリーしていたときはそのプレイヤーとの対戦が開始されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
public class PuyoMatchHub : Hub { async Task OnMatchingEstablished(PlayerInfo player) { KeyValuePair<string, PlayerInfo> keyPair = WaitingPlayers.FirstOrDefault(_ => _.Value != player); if (keyPair.Value == null) return; string[] connectionIds = new string[2]; connectionIds[0] = player.ConnectionId; connectionIds[1] = keyPair.Value.ConnectionId; WaitingPlayers.Remove(keyPair.Value.ConnectionId); // 接続されているユーザーに登録されている人数が変更されたことを通知する foreach (IClientProxy client in ClientProxyMap.Values) await client.SendAsync("PlayerCountChangedToClient", WaitingPlayers.Count); PlayerInfo[] playerInfos = new PlayerInfo[2]; playerInfos[0] = player; playerInfos[1] = keyPair.Value; player.CheckSelfProduced(keyPair.Value); PuyoMatchGame game = new PuyoMatchGame(playerInfos); AddEventHandlers(game); Games.Add(game); foreach (IClientProxy client in ClientProxyMap.Values) _ = client.SendAsync("GameCountChangedToClient", Games.Count); await Task.Delay(1000); string[] esNames = new string[2]; esNames[0] = player.EscapedPlayerName; esNames[1] = keyPair.Value.EscapedPlayerName; string str1 = $"【マッチング成功】{YellowTag}{esNames[0]}{SpanTagEnd} さんの対戦相手は {RedTag}{esNames[1]}{SpanTagEnd} さんです。"; string str2 = $"【マッチング成功】{YellowTag}{esNames[1]}{SpanTagEnd}さんの対戦相手が見つかりました。{RedTag}{esNames[0]}{SpanTagEnd} さんです。"; if (ClientProxyMap.ContainsKey(connectionIds[0])) await ClientProxyMap[connectionIds[0]].SendAsync("MatchingSucceededToClient", str1); if (ClientProxyMap.ContainsKey(connectionIds[1])) await ClientProxyMap[connectionIds[1]].SendAsync("MatchingSucceededToClient", str2); await Task.Delay(3000); game.GameStart(); if (ClientProxyMap.ContainsKey(connectionIds[0])) await ClientProxyMap[connectionIds[0]].SendAsync("EventGameStartToClient", true); if (ClientProxyMap.ContainsKey(connectionIds[1])) await ClientProxyMap[connectionIds[1]].SendAsync("EventGameStartToClient", false); await SendMatchString(connectionIds[0], esNames[0], esNames[1], 1); await SendMatchString(connectionIds[1], esNames[1], esNames[0], 1); CheckGameAbandoned(connectionIds, esNames); SendGamesToClients(); } } |
エントリーしたときにエントリーしている人が誰もいないときは待機することになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PuyoMatchHub : Hub { async Task OnMatchingFailed(PlayerInfo player) { WaitingPlayers.Add(player.ConnectionId, player); string str = $"{RedTag}[{player.EscapedPlayerName}] でエントリーしました。対戦相手が見つかるまでしばらくお待ちください{SpanTagEnd}"; if (ClientProxyMap.ContainsKey(player.ConnectionId)) await ClientProxyMap[player.ConnectionId].SendAsync("EnteredToClient", str); // 接続されているユーザーに登録されている人数が変更されたことを通知する List<Task> tasks = new List<Task>(); foreach (IClientProxy client in ClientProxyMap.Values) tasks.Add(client.SendAsync("PlayerCountChangedToClient", WaitingPlayers.Count)); Task.WaitAll(tasks.ToArray()); // ただし自分自身には自分以外の登録人数を通知する if (ClientProxyMap.ContainsKey(player.ConnectionId)) await ClientProxyMap[player.ConnectionId].SendAsync("PlayerCountChangedToClient", WaitingPlayers.Count(_ => _.Value != player)); } } |
ひとつのゲームが終了したら片方の勝ち数が3になっているか調べ、勝者がログインユーザーの場合は記録を保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class PuyoMatchHub : Hub { private async void Game_LostGame(PuyoMatchGame game, bool isPlayer) { if (isPlayer) game.WinCounts[1]++; else game.WinCounts[0]++; await SendGameResult(game, isPlayer); bool ret = await CheckGameSet(game, isPlayer); if (ret) // ret == true ならゲームセット { try { SaveWinnerScore(game); } catch { } return; } // それ以外はそのまま await Task.Delay(3000); game.NextGameStart(false); await SendNextGameStarted(game, isPlayer); CheckGameAbandoned(game.ConnectionIds, game.EscapedPlayerNames); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
public class PuyoMatchHub : Hub { void SaveWinnerScore(PuyoMatchGame game) { if (game.WinCounts[0] >= PuyoMatchGame.WIN_COUNT_MAX && ClientProxyMap.ContainsKey(game.ConnectionIds[0])) { // ログインしているユーザーの成績だけ保存する if (game.IsLoginUsers[0]) SaveScoreToFile(game.PlayerNames[0], game.Scores[0]); } else if (game.WinCounts[1] >= PuyoMatchGame.WIN_COUNT_MAX && ClientProxyMap.ContainsKey(game.ConnectionIds[1])) { if (game.IsLoginUsers[1]) SaveScoreToFile(game.PlayerNames[1], game.Scores[1]); } } void SaveScoreToFile(string loginPlayerName, int score) { if (File.Exists(LoginModel.ResistPath)) { // テキストファイルの内容を1行ずつリストに格納する // ただし書き換える行は書き換える文字列を格納する List<string> strings = new List<string>(); StreamReader sr = new StreamReader(LoginModel.ResistPath, Encoding.UTF8); while (true) { string? str = sr.ReadLine(); if (str == null) break; string[] vs = str.Split('\t'); string playerName = vs[0]; if (playerName == loginPlayerName) { // ユーザーネームと同じであればそこが書き換える行である // 勝ち点を1増やし、スコアを総得点に追加する int winCount = int.Parse(vs[3]) + 1; int totalScore = int.Parse(vs[4]) + score; strings.Add($"{playerName}\t{vs[1]}\t{vs[2]}\t{winCount}\t{totalScore}"); Console.WriteLine($"{playerName}\t{vs[1]}\t{vs[2]}\t{winCount}\t{totalScore}"); } else // 書き換える行でないならそのままリストに格納する strings.Add(str); } sr.Close(); // リストに格納された文字列をファイルに上書き保存する StreamWriter sw = new StreamWriter(LoginModel.ResistPath, false, Encoding.UTF8); foreach (string str in strings) sw.WriteLine(str); sw.Close(); } } } |
ランキングの表示
ランキングを表示するためにScoreクラスを定義します。
1 2 3 4 5 6 7 8 9 |
namespace PuyoMatch { public class Score { public string Name = ""; public int WinCount = 0; public int TotalScore = 0; } } |
Pagesフォルダの配下にランキングを表示するためのcshtmlファイルを作成します。
ページにアクセスされたらサーバー上のファイルを読み込んでScoreオブジェクトのリストを作成します。これをつかってスコア一覧を表示させます。
Pages\Puyo-Match\ranking.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
@page @using PuyoMatch @{ Layout = ""; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>鳩でもわかる「ぷよぷよ」ならぬ「ぴよぴよ」 上位30位</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> </head> <body> @{ StreamReader sr = new StreamReader(LoginModel.ResistPath, System.Text.Encoding.UTF8); List<Score> scores = new List<Score>(); while (true) { string? str = sr.ReadLine(); if (str == null) break; string[] vs = str.Split('\t'); if (int.Parse(vs[3]) == 0) // 総得点が0のものは無視 continue; Score score = new Score(); score.Name = vs[0]; score.WinCount = int.Parse(vs[3]); score.TotalScore = int.Parse(vs[4]); scores.Add(score); } sr.Close(); // 勝ち点順、同位であれば総得点順にソートし、上位30位を表示させる scores = scores.OrderByDescending(_ => _.WinCount).ThenByDescending(_ => _.TotalScore).Take(30).ToList(); } <div id = "container"> <div id = "h1">鳩でもわかる「ぷよぷよ」ならぬ「ぴよぴよ」 上位30位</div> <div id = "left"> <p><a href="./game">⇒ 「ぷよぷよ」ならぬ「ぴよぴよ」のページへ戻る</a></p> <div id = "result" > <table class="table" border="1" id="table"> <tr> <td>順位</td> <td>プレイヤー</td> <td>勝ち数</td> <td>総得点</td> </tr> @{ int num = 0; } @foreach (Score score in scores) { num++; string totalScore = string.Format("{0:#,0}", score.TotalScore); <tr> <td>@num 位</td> <td>@score.Name</td> <td>@score.WinCount</td> <td>@totalScore</td> </tr> } @if (num < 30) { @for (num++; num <= 30; num++) { <tr> <td>@num 位</td> <td></td> <td></td> <td></td> </tr> } } </table> </div> </div> </div> </body> </html> |