ASP.NET Core版 対人対戦できるぷよぷよをつくる(7)の続きです。前回はユーザー登録してプレイした人の記録を残す機能を実装しました。今回は開始から勝敗が決するまでの過程(将棋や囲碁でいうところの棋譜)を残すことができるように改良します。
Contents
棋譜のフォーマット
まず棋譜のフォーマットを決めます。ここから参照できます。
最初の1行は2人の対戦者の名前と勝者(0か1か)がタブ区切りになっています。空白行が2行あって次にあるのが対戦開始からの経過秒です。
そのあと1行の空白行があってカンマ区切りされた “-1_0,-1_0,-1_0,3_0,0_0,0_0″のような行が13列あります。整数値がアンダーバー(”_”)でつながっていますが、これはぷよの種類と連鎖発生時に表示される数字です。連鎖が発生していないときは右側の数字はすべて0になります。
これが2セットあり、そのあと空白行1行のあと”4_2_4_4,4_2_4_4″のようなカンマとアンダーバーで区切られた数字がありますが、これがネクストぷよです。カンマで両プレイヤーのネクストぷよが区切られていて、それぞれがアンダーバーで区切られていて、それぞれが2個で1セットの次回と次々回のぷよになっています。
さらにカンマで区切られた数字があります。これは両者のスコアです。その下にさらにカンマで区切られた数字があります。これが予告ぷよの数です。
さらにそのあとに空白行が2行ありますが、ここから次のサイクルになります。
ではこれらの文字列を生成するためのメソッドを示します。PuyoMatchGameクラスに処理を追加します。
フィールド上に存在するぷよを文字列に変換
以下はフィールド上に存在するぷよを文字列に変換するためのメソッドです。対象はすでに固定されているぷよだけではなく、落下中の組ぷよ、落下中のおじゃまぷよも含みます。
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 |
public class PuyoMatchGame { public string GetPuyosString() { List<string> strings = new List<string>(); for (int i = 0; i < 2; i++) { // 落下中のくみぷよを取得 Fields[i].GetFallingPuyos(out FallingPuyo[] fallingPuyos, out FallingPuyo[] nextPuyos1, out FallingPuyo[] nextPuyos2); PuyoType[] fallingTypes = new PuyoType[2]; int[] fallingRows = new int[2]; int[] fallingCols = new int[2]; // fallingPuyos[0].Row == -1 または fallingPuyos[1].Row == -1 のときは // 組ぷよが着地してつぎの組ぷよが落ちてくる間の時間帯である // それ以外のときは組ぷよがフィールド上に表示されているときなので // 組ぷよの種類、位置を変数に格納する if (fallingPuyos[0].Row != -1 && fallingPuyos[1].Row != -1) { fallingRows[0] = fallingPuyos[0].Row; fallingRows[1] = fallingPuyos[1].Row; fallingCols[0] = fallingPuyos[0].Col; fallingCols[1] = fallingPuyos[1].Col; fallingTypes[0] = fallingPuyos[0].Type; fallingTypes[1] = fallingPuyos[1].Type; } else { fallingTypes[0] = PuyoType.None; fallingTypes[1] = PuyoType.None; } // 固定されたぷよを取得 Puyo[,] fixedPuyos = Fields[i].CopiedFixPuyos; // 落下中の組ぷよが存在するなら固定されたぷよと統合する if (fallingTypes[0] != PuyoType.None) { fixedPuyos[fallingRows[0], fallingCols[0]] = new Puyo(fallingTypes[0]); fixedPuyos[fallingRows[1], fallingCols[1]] = new Puyo(fallingTypes[1]); } // 落下中のおじゃまぷよがあるなら固定されたぷよと統合する var fallingOjama = Fields[i].GetFallingOjamaPuyos(); foreach (var ojama in fallingOjama) { if (ojama.Row >= 0) fixedPuyos[ojama.Row, ojama.Col] = new Puyo(PuyoType.Ojama); } // ぷよの種類をカンマと改行区切りの文字列に変換する int rowMax = fixedPuyos.GetLength(0); int colMax = fixedPuyos.GetLength(1); List<string> puyosStrings = new List<string>(); for (int row = 0; row < rowMax; row++) { List<string> puyoInfo = new List<string>(); for (int col = 0; col < colMax; col++) { int type = (int)fixedPuyos[row, col].Type; int rensa = fixedPuyos[row, col].Rensa; puyoInfo.Add($"{type}_{rensa}"); } puyosStrings.Add(String.Join(",", puyoInfo)); } strings.Add(string.Join("\n", puyosStrings)); } // プレイヤーは2人いるので、それぞれのあいだに空白行をいれる return string.Join("\n\n", strings); } } |
棋譜として保存する文字列を生成する
以下は上記も含めて棋譜として保存する文字列をすべて取得するメソッドです。
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 |
public class PuyoMatchGame { string GetStringForSave() { List<string> strings = new List<string>(); strings.Add(GetPuyosString()); // 両プレイヤーのネクストぷよを文字列に変換 List<string> nextsStrings = new List<string>(); for (int i = 0; i < 2; i++) { // それぞれのネクストぷよを文字列に変換 int[] ints = new int[4]; Fields[i].GetFallingPuyos(out FallingPuyo[] fallingPuyos, out FallingPuyo[] nextPuyos1, out FallingPuyo[] nextPuyos2); ints[0] = (int)nextPuyos1[0].Type; ints[1] = (int)nextPuyos1[1].Type; ints[2] = (int)nextPuyos2[0].Type; ints[3] = (int)nextPuyos2[1].Type; nextsStrings.Add(String.Join("_", ints)); } strings.Add(string.Join(",", nextsStrings)); // 両プレイヤーのスコアを文字列に変換 List<string> scoresStrings = new List<string>(); for (int i = 0; i < 2; i++) scoresStrings.Add(Scores[i].ToString()); strings.Add(string.Join(",", scoresStrings)); List<string> ojamasStrings = new List<string>(); for (int i = 0; i < 2; i++) ojamasStrings.Add(OjamaCounts[i].ToString()); strings.Add(string.Join(",", ojamasStrings)); return string.Join("\n\n", strings); } } |
棋譜にはゲーム開始時からの経過時間も保存したいので、ゲーム開始時に現在時刻をフィールド変数に格納する処理を追加します。
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 |
public class PuyoMatchGame { DateTime _dtGameStart = DateTime.Now; public void GameStart() { // ゲーム開始時刻をフィールド変数に格納しておく _dtGameStart = DateTime.Now; int seed = (int)(_dtGameStart.Ticks % int.MaxValue); // あとは既存の処理 Task.Run(async () => { await Fields[0].Init(seed); await Fields[1].Init(seed); Scores[0] = 0; Scores[1] = 0; OjamaCounts[0] = 0; OjamaCounts[1] = 0; IsPlaying = true; _timerForPuyoDown.Start(); }); } } |
2戦目以降がはじまるときも開始時刻を初期化します。
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 |
public class PuyoMatchGame { public void NextGameStart(bool isScoreRest) { _dtGameStart = DateTime.Now; int seed = (int)(_dtGameStart.Ticks % int.MaxValue); Task.Run(async () => { await Fields[0].Init(seed); await Fields[1].Init(seed); if (isScoreRest) { Scores[0] = 0; Scores[1] = 0; } OjamaCounts[0] = 0; OjamaCounts[1] = 0; IsPlaying = true; _timerForPuyoDown.Start(); }); } } |
StringBuilderクラスを利用して棋譜として文字列を保存する
以下のような状態の変化が起きた場合は、これを保存します。
ぷよが着地したとき
連鎖が発生したとき
おじゃまぷよの落下が完了したとき
勝敗が決したとき
上記のときでGetStringForSaveメソッドが実行された結果、前回取得された文字列と違いがある場合、メモリー上に保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class PuyoMatchGame { StringBuilder _sbToSave = new StringBuilder(); string _prevStringToSave = ""; void SaveStringToMemory() { // GetStringForSaveメソッドを実行した結果、前回取得できた文字列と異なる場合のみ保存する string str = GetStringForSave(); if (str != _prevStringToSave) { _prevStringToSave = str; // ゲーム開始時からの経過秒を取得して、これも保存する TimeSpan ts = DateTime.Now - _dtGameStart; string s = ((int)ts.TotalSeconds).ToString() + "\n\n"; // もちろん GetStringForSaveメソッドを実行した結果、取得された文字列も保存する s += GetStringForSave() + "\n\n\n"; _sbToSave.Append(s); } } } |
SaveStringToMemoryメソッドを実行するのは、ぷよが着地したとき、連鎖が発生したとき、おじゃまぷよの落下が完了したとき、勝敗が決したときです。そこでこれらが起きたとき呼び出されるイベントハンドラ内でSaveStringToMemoryメソッドを呼び出します。
着地したときは連鎖がまったく発生しないときでもBeginRensaとFinishRensaイベントは発生します。そこでここでSaveStringToMemoryメソッドを呼び出します。
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 |
public class PuyoMatchGame { private void FieldPlayer0_BeginRensa(object? sender, EventArgs e) { _tempOjamaCounts[0] = 0; _addScore0 = 0; SaveStringToMemory(); } private void FieldPlayer1_BeginRensa(object? sender, EventArgs e) { _tempOjamaCounts[1] = 0; _addScore1 = 0; SaveStringToMemory(); } private void FieldPlayer0_FinishRensa(object? sender, EventArgs e) { if (Fields[1].GetYokokuCount() < OjamaCounts[1]) Fields[1].SetOjamaPuyo(OjamaCounts[1] - Fields[1].GetYokokuCount()); SaveStringToMemory(); } private void FieldPlayer1_FinishRensa(object? sender, EventArgs e) { if (Fields[0].GetYokokuCount() < OjamaCounts[0]) Fields[0].SetOjamaPuyo(OjamaCounts[0] - Fields[0].GetYokokuCount()); SaveStringToMemory(); } } |
連鎖が進行しているときもSaveStringToMemoryメソッドを呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class PuyoMatchGame { private void FieldPlayer0_Rensa(int rensa, int addScore) { CalculateOjamapuyo(rensa, addScore, true); SaveStringToMemory(); } private void FieldPlayer1_Rensa(int rensa, int addScore) { CalculateOjamapuyo(rensa, addScore, false); SaveStringToMemory(); } } |
おじゃまぷよの落下が完了したときもSaveStringToMemoryメソッドを呼び出します。コンストラクタ内でOjamaFalledのイベントハンドラを追加しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class PuyoMatchGame { // コンストラクタ内に以下の2行を追加 // Fields[0].OjamaFalled += FieldPlayer0_OjamaFalled; // Fields[1].OjamaFalled += FieldPlayer1_OjamaFalled; private void FieldPlayer0_OjamaFalled(int count) { SaveStringToMemory(); } private void FieldPlayer1_OjamaFalled(int count) { SaveStringToMemory(); } } |
棋譜をファイルとして保存する
決着がついたらこれをテキストファイルとして保存します。
テキストファイルを保存する場所はアプリケーションをアップロードするフォルダの外側にしています。これはメンテナンスのときに間違ってローカル上のファイルで上書きしてしまわないようにするためで、とくに意味はありません。
ファイル名で保存された時刻がわかるようにしています。また他の対戦が同時に終わったとき例外が発生することがあるので例外処理をしています。
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 |
public class PuyoMatchGame { void SaveStringToFile(int winnner) { // 保存用のディレクトリがないなら作る if (!Directory.Exists("../puyo-match-data/")) Directory.CreateDirectory("../puyo-match-data/"); // ファイルの名前を決める string filePath = "../puyo-match-data/" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".txt"; // 同じタイミングで書き込みがあるかもしれないので同名のファイルがある場合はなにもしない。 // 例外処理もしておく。 if (!File.Exists(filePath)) { try { StreamWriter sw = new StreamWriter(filePath); // 最初の1行にプレイヤー名と勝者がわかるようにしておく sw.Write(EscapedPlayerNames[0] + "\t" + EscapedPlayerNames[1] + "\t" + winnner.ToString() + "\n\n\n"); sw.Write(_sbToSave.ToString()); sw.Close(); } catch { } } // StringBuilderオブジェクトをクリアする _sbToSave.Clear(); } } |
このメソッドを実行するのはGameOveredイベントハンドラです。
たまに同時に窒息してしまうことがあります。そこで両方が勝者になってしまわないように IsPlaying が false のときはなにもしません。これで同時に窒息した場合も片方が勝者、他方が敗者になることを保証しています。
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 |
public class PuyoMatchGame { private void FieldPlayer0_GameOvered(object? sender, EventArgs e) { if (IsPlaying) { IsPlaying = false; _timerForPuyoDown.Stop(); LostGame?.Invoke(this, true); // 引数は勝者。したがって自分でない側 SaveStringToFile(1); } } private void FieldPlayer1_GameOvered(object? sender, EventArgs e) { if (IsPlaying) { IsPlaying = false; _timerForPuyoDown.Stop(); LostGame?.Invoke(this, false); SaveStringToFile(0); } } } |