ASP.NET Core版 対コンピュータ対戦できるぷよぷよをつくる(1)の続きです。今回はゲームの進行全体を管理するPuyoGameクラスを定義します。
Contents
PuyoGameクラスの定義
1 2 3 4 5 6 |
namespace Puyo { public class PuyoGame { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class PuyoGame { } |
各プロパティ
ConnectionIdプロパティはAspNetCore.SignalRでサーバーサイドに接続するときに付与されるIDです。PlayerNameはプレイヤー名です。プレイヤー名で空文字列を指定したときは自動的に”名無しさん”となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class PuyoGame { public string ConnectionId { get; } string _playerName = ""; public string PlayerName { set { _playerName = value; if (_playerName == "") _playerName = "名無しさん"; } get { return _playerName; } } } |
ぷよを動かすにあたってプレイヤー用とコンピュータ側のFieldオブジェクトを作成します。FieldsにはFieldオブジェクトの配列が格納されます。またScoresプロパティにはプレイヤー用とコンピュータ側双方のスコアが格納されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class PuyoGame { public Field[] Fields { private set; get; } public int[] Scores { private set; get; } } |
サーバーサイドでデータが変更されたらそれをクライアントサイドに送信することになりますが、FixedPuyoRowsTextsプロパティ、FixedPuyoColsTextsプロパティ、FixedPuyoTypesTextsプロパティ、FixedPuyoRensasTextsプロパティにはぷよの行、列、種類、連鎖数がカンマ区切りの文字列として格納されます。
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 PuyoGame { public string[] FixedPuyoRowsTexts { private set; get; } public string[] FixedPuyoColsTexts { private set; get; } public string[] FixedPuyoTypesTexts { private set; get; } public string[] FixedPuyoRensasTexts { private set; get; } } |
FallingPuyoRowsTextsプロパティ、FallingPuyoColsTextsプロパティ、FallingPuyoTypesTextsプロパティには落下中のぷよの行、列、種類がカンマ区切りの文字列として格納されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class PuyoGame { public string[] FallingPuyoRowsTexts { private set; get; } public string[] FallingPuyoColsTexts { private set; get; } public string[] FallingPuyoTypesTexts { private set; get; } } |
コンストラクタ
上記の各プロパティを初期化します。そしてFieldオブジェクトにイベントハンドラを追加します。
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 |
public class PuyoGame { System.Timers.Timer _timerForPuyoDown = new System.Timers.Timer(); public PuyoGame(string connectionId) { ConnectionId = connectionId; Fields = new Field[2]; Scores = new int[2]; FixedPuyoRowsTexts = new string[2]; FixedPuyoColsTexts = new string[2]; FixedPuyoTypesTexts = new string[2]; FixedPuyoRensasTexts = new string[2]; FallingPuyoRowsTexts = new string[2]; FallingPuyoColsTexts = new string[2]; FallingPuyoTypesTexts = new string[2]; Fields[0] = new Field(true); Fields[1] = new Field(false); Fields[0].Rensa += FieldPlayer1_Rensa; // イベントハンドラは後述 Fields[1].Rensa += FieldPlayer2_Rensa; Fields[0].Updated += FieldPlayer1_Updated; Fields[1].Updated += FieldPlayer2_Updated; Fields[0].SendNext += FieldPlayer1_SendNext; Fields[1].SendNext += FieldPlayer2_SendNext; Fields[0].ScoreChanged += FieldPlayer1_ScoreChanged; ; Fields[1].ScoreChanged += FieldPlayer2_ScoreChanged; Fields[0].GameOvered += FieldPlayer1_GameOvered; Fields[1].GameOvered += FieldPlayer2_GameOvered; _timerForPuyoDown.Interval = 1000; // ぷよの自然落下は1秒につき1回とする _timerForPuyoDown.Elapsed += Timer_Elapsed; Fields[0].OjamaFalled += FieldPlayer1_OjamaFalled; Fields[1].OjamaFalled += FieldPlayer2_OjamaFalled; Scores[0] = 0; Scores[1] = 0; } } |
ゲーム開始の処理
ゲームを開始するときは各Fieldオブジェクトを初期化してスコアを0にリセットします。そしてぷよを自然落下させるためのタイマーをStartさせます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class PuyoGame { bool isPlaying = false; public async Task GameStart(string playerName) { PlayerName = playerName; await Fields[0].Init(); await Fields[1].Init(); Scores[0] = 0; Scores[1] = 0; isPlaying = true; _timerForPuyoDown.Start(); } } |
StopTimerForPuyoDownメソッドはゲーム終了時や次のステージに移行しているときにぷよを自然落下させるためのタイマーを停止するためのものです。
1 2 3 4 5 6 7 |
public class PuyoGame { public void StopTimerForPuyoDown() { _timerForPuyoDown.Stop(); } } |
ぷよの移動、回転、落下の処理
ユーザーがキー操作でぷよを移動、回転、落下させるための処理を示します。Fieldクラスの対応したメソッドを呼び出しているだけですが、回転に成功した場合と急速落下させたぷよが着地したときは効果音を鳴らすためにイベントを発生させます。
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 |
public class PuyoGame { public delegate void GameHandler(PuyoGame game); public event GameHandler? Rotated; public event GameHandler? Downed; public void StopTimerForPuyoDown() { _timerForPuyoDown.Stop(); } public void MoveLeft() { if (!_timerForPuyoDown.Enabled) return; Fields[0].MoveLeft(); } public void MoveRight() { if (!_timerForPuyoDown.Enabled) return; Fields[0].MoveRight(); } public void RotateLeft() { if (Fields[0].Rotate(false)) Rotated?.Invoke(this); } public void RotateRight() { if (Fields[0].Rotate(true)) Rotated?.Invoke(this); } public void MoveDown() { if (!_timerForPuyoDown.Enabled) return; Task.Run(async () => { await Fields[0].FallAll(); Downed?.Invoke(this); }); } } |
連鎖時の処理
連鎖が発生したとき(1連鎖のみの場合を含む)も効果音を鳴らしたい(プレイヤー用とコンピュータ側で効果音を変えたい)ので、このときもイベントを発生させてクライアントサイドに通知できるようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class PuyoGame { public event GameHandler? PlayerRensa; private void FieldPlayer1_Rensa(object? sender, EventArgs e) { PlayerRensa?.Invoke(this); } public event GameHandler? CpuRensa; private void FieldPlayer2_Rensa(object? sender, EventArgs e) { CpuRensa?.Invoke(this); } } |
加点時の処理
点数が加算されたときの処理を示します。点数が加算されるときとは連鎖が完了したときで、敵側に送り込むぷよの数を計算して確定させなければなりません。計算式は(追加点 ÷ 70)とします。実際にはおじゃまレートなる要素も必要なのですが、ここでは考えないことにします。この計算式ではぷよを4個つなげて消すだけの連鎖なし(1連鎖)では1個もおじゃまぷよを送り込むことができないことになります。
また自分のフィールドに降ってくるおじゃまぷよがすでに存在するときはそれと相殺する処理がおこなわれます。相殺が発生したらこれもクライアントサイドで処理をさせたいので、イベントを発生させます。
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 |
public class PuyoGame { public event GameHandler? Offseted; private void FieldPlayer1_ScoreChanged(int addScore) { Scores[0] += addScore; if(addScore > 0) { int ojamaCount = addScore / 70; int afterOjamaCount = Fields[0].RemoveOjamaPuyo(ojamaCount); // 相殺はあるか? // ojamaCount と afterOjamaCount が異なる場合は相殺が発生している if (ojamaCount != afterOjamaCount) Offseted?.Invoke(this); // 相殺した結果、残ったものは敵側に送り込む if(afterOjamaCount > 0) Fields[1].SetOjamaPuyo(afterOjamaCount); } } private void FieldPlayer2_ScoreChanged(int addScore) { Scores[1] += addScore; if (addScore > 0) { int ojamaCount = addScore / 70; int afterOjamaCount = Fields[1].RemoveOjamaPuyo(ojamaCount); if (ojamaCount != afterOjamaCount) Offseted?.Invoke(this); if(afterOjamaCount > 0) Fields[0].SetOjamaPuyo(afterOjamaCount); } } } |
おじゃまぷよの落下処理
おじゃまぷよの落下処理がおこなわれるときは効果音を鳴らしたいので、この場合もイベントを発生させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class PuyoGame { public event OjamaFalledHandler? OjamaFalled; public delegate void OjamaFalledHandler(PuyoGame game, bool isPlayer, int ojamaCount); private void FieldPlayer1_OjamaFalled(bool isPlayer, int ojamaCount) { OjamaFalled?.Invoke(this, true, ojamaCount); } private void FieldPlayer2_OjamaFalled(bool isPlayer, int ojamaCount) { OjamaFalled?.Invoke(this, false, ojamaCount); } } |
ネクストぷよ出現時の処理
落下中の組ぷよが着地したらクライアントサイドでは次の組ぷよの表示を切り替えなければなりません。そのためのイベントを発生させます。
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 |
public class PuyoGame { public delegate void SendNextHandler( PuyoGame game, bool isPlayer, string mainSub, string nextMainSub, string nextNextMainSub); public event SendNextHandler? SendNext; int GetNumberFromPuyo(FallingPuyo puyo) { if (puyo.Type == PuyoType.None) return 0; else if (puyo.Type == PuyoType.Type1) return 1; else if (puyo.Type == PuyoType.Type2) return 2; else if (puyo.Type == PuyoType.Type3) return 3; else if (puyo.Type == PuyoType.Type4) return 4; else if (puyo.Type == PuyoType.Ojama) return -1; else return 0; } string CreateTextFromNextPuyo(FallingPuyo main, FallingPuyo sub) { int[] ints = new int[2]; ints[0] = GetNumberFromPuyo(main); ints[1] = GetNumberFromPuyo(sub); return String.Join(",", ints); } private void FieldPlayer1_SendNext(FallingPuyo main, FallingPuyo sub, FallingPuyo nextMain1, FallingPuyo nextSub1, FallingPuyo nextMain2, FallingPuyo nextSub2) { string mainSub = CreateTextFromNextPuyo(main, sub); string nextMainSub = CreateTextFromNextPuyo(nextMain1, nextSub1); string nextNextMainSub = CreateTextFromNextPuyo(nextMain2, nextSub2); SendNext?.Invoke(this, true, mainSub, nextMainSub, nextNextMainSub); } private void FieldPlayer2_SendNext(FallingPuyo main, FallingPuyo sub, FallingPuyo nextMain1, FallingPuyo nextSub1, FallingPuyo nextMain2, FallingPuyo nextSub2) { string mainSub = CreateTextFromNextPuyo(main, sub); string nextMainSub = CreateTextFromNextPuyo(nextMain1, nextSub1); string nextNextMainSub = CreateTextFromNextPuyo(nextMain2, nextSub2); SendNext?.Invoke(this, false, mainSub, nextMainSub, nextNextMainSub); } } |
ゲームオーバー時の処理
ゲームオーバーになったときの処理を示します。ユーザー側がゲームオーバーになったら文字通りのゲームオーバーです。コンピュータ側がゲームオーバーになったらステージクリアです。そのための処理を示します。
どちらの場合もイベントを発生させます。ユーザーがゲームオーバーになった場合はスコアランキングへの登録をおこないます。ステージクリアの場合はタイマーを一時停止して2秒後にFieldオブジェクトを初期化してゲームを再開します。この場合はコンピュータ側のスコアを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 |
public class PuyoGame { public delegate void GameOveredHandler(PuyoGame game, bool isPlayer); public event GameOveredHandler? GameOvered; public event GameOveredHandler? StageCleared; private void FieldPlayer1_GameOvered(Puyo[,] field, FallingPuyo? mainPuyo, FallingPuyo? subPuyo, List<FallingPuyo> fallingOjamaPuyos) { _timerForPuyoDown.Stop(); GameOvered?.Invoke(this, true); string path = "../hiscore-puyo-1.txt"; HiscoreManager.Save(path, PlayerName, Scores[0]); } private void FieldPlayer2_GameOvered(Puyo[,] field, FallingPuyo? mainPuyo, FallingPuyo? subPuyo, List<FallingPuyo> fallingOjamaPuyos) { StageCleared?.Invoke(this, false); _timerForPuyoDown.Stop(); Task.Run(async () => { await Task.Delay(2000); await Fields[0].Init(); await Fields[1].Init(); _timerForPuyoDown.Start(); Scores[1] = 0; }); } } |
更新処理用データを送信するための処理
AspNetCore.SignalRクラスでクライアントサイドに描画更新用のデータを送信することになりますが、送信すべきデータを作る処理が必要です。以下の処理は固定されたぷよと落下中のぷよのRowプロパティやColプロパティをカンマ区切りの文字列にしたものをPuyoGameクラスの各プロパティにセットします。
セットされた文字列はAspNetCore.SignalRクラス側から1秒間に30回くらいの頻度で取得され、クライアントサイドに送信されます。
これはPuyo.Typeプロパティを数字に変換するためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class PuyoGame { int GetNumberFromPuyo(Puyo puyo) { if (puyo.Type == PuyoType.None) return 0; else if (puyo.Type == PuyoType.Type1) return 1; else if (puyo.Type == PuyoType.Type2) return 2; else if (puyo.Type == PuyoType.Type3) return 3; else if (puyo.Type == PuyoType.Type4) return 4; else if (puyo.Type == PuyoType.Ojama) return -1; else return 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 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 |
public class PuyoGame { string CreateFixedPuyoRowsText(Puyo[,] field) { int rowMax = field.GetLength(0); int colMax = field.GetLength(1); List<int> rows = new List<int>(); for (int row = 0; row < rowMax; row++) { for (int col = 0; col < colMax; col++) rows.Add(row); } return String.Join(",", rows); } string CreateFixedPuyoColsText(Puyo[,] field) { int rowMax = field.GetLength(0); int colMax = field.GetLength(1); List<int> cols = new List<int>(); for (int row = 0; row < rowMax; row++) { for (int col = 0; col < colMax; col++) cols.Add(col); } return String.Join(",", cols); } string CreateFixedPuyoTypesText(Puyo[,] field) { int rowMax = field.GetLength(0); int colMax = field.GetLength(1); List<int> types = new List<int>(); for (int row = 0; row < rowMax; row++) { for (int col = 0; col < colMax; col++) { types.Add(GetNumberFromPuyo(field[row, col])); } } return String.Join(",", types); } string CreateFixedPuyoRensasText(Puyo[,] field) { int rowMax = field.GetLength(0); int colMax = field.GetLength(1); List<int> rensas = new List<int>(); for (int row = 0; row < rowMax; row++) { for (int col = 0; col < colMax; col++) rensas.Add(field[row, col].Rensa); } return String.Join(",", rensas); } string CreateFallingPuyoRowsText(FallingPuyo? mainPuyo, FallingPuyo? subPuyo, List<FallingPuyo> ojamaPuyos) { List<int> fallingRows = new List<int>(); fallingRows.AddRange(ojamaPuyos.Select(_ => _.Row)); if (mainPuyo != null && subPuyo != null) { fallingRows.Add(mainPuyo.Row); fallingRows.Add(subPuyo.Row); } return String.Join(",", fallingRows); } string CreateFallingPuyoColsText(FallingPuyo? mainPuyo, FallingPuyo? subPuyo, List<FallingPuyo> ojamaPuyos) { List<int> fallingCols = new List<int>(); fallingCols.AddRange(ojamaPuyos.Select(_ => _.Col)); if (mainPuyo != null && subPuyo != null) { fallingCols.Add(mainPuyo.Col); fallingCols.Add(subPuyo.Col); } return String.Join(",", fallingCols); } string CreateFallingPuyoTypesText(FallingPuyo? mainPuyo, FallingPuyo? subPuyo, List<FallingPuyo> ojamaPuyos) { List<int> fallingTypes = new List<int>(); fallingTypes.AddRange(ojamaPuyos.Select(_ => GetNumberFromPuyo(_))); if (mainPuyo != null && subPuyo != null) { fallingTypes.Add(GetNumberFromPuyo(mainPuyo)); fallingTypes.Add(GetNumberFromPuyo(subPuyo)); } return String.Join(",", fallingTypes); } } |
Fieldクラス内でぷよに関するデータが変更されたときはイベントハンドラにおいて以下のような処理がおこなわれます。
やっていることは上述のメソッドでカンマ区切りの文字列を取得して、PuyoGameクラスのプロパティに格納しているだけです。
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 |
public class PuyoGame { private void FieldPlayer1_Updated(Puyo[,] field, FallingPuyo? mainPuyo, FallingPuyo? subPuyo, List<FallingPuyo> fallingOjamaPuyos) { FixedPuyoRowsTexts[0] = CreateFixedPuyoRowsText(field); FixedPuyoColsTexts[0] = CreateFixedPuyoColsText(field); FixedPuyoTypesTexts[0] = CreateFixedPuyoTypesText(field); FixedPuyoRensasTexts[0] = CreateFixedPuyoRensasText(field); FallingPuyoRowsTexts[0] = CreateFallingPuyoRowsText(mainPuyo, subPuyo, fallingOjamaPuyos); FallingPuyoColsTexts[0] = CreateFallingPuyoColsText(mainPuyo, subPuyo, fallingOjamaPuyos); FallingPuyoTypesTexts[0] = CreateFallingPuyoTypesText(mainPuyo, subPuyo, fallingOjamaPuyos); } private void FieldPlayer2_Updated(Puyo[,] field, FallingPuyo? mainPuyo, FallingPuyo? subPuyo, List<FallingPuyo> fallingOjamaPuyos) { FixedPuyoRowsTexts[1] = CreateFixedPuyoRowsText(field); FixedPuyoColsTexts[1] = CreateFixedPuyoColsText(field); FixedPuyoTypesTexts[1] = CreateFixedPuyoTypesText(field); FixedPuyoRensasTexts[1] = CreateFixedPuyoRensasText(field); FallingPuyoRowsTexts[1] = CreateFallingPuyoRowsText(mainPuyo, subPuyo, fallingOjamaPuyos); FallingPuyoColsTexts[1] = CreateFallingPuyoColsText(mainPuyo, subPuyo, fallingOjamaPuyos); FallingPuyoTypesTexts[1] = CreateFallingPuyoTypesText(mainPuyo, subPuyo, fallingOjamaPuyos); } } |
ぷよの自然落下とCPU側の動作
1秒おきに以下の処理がおこなわれます。ユーザー側の落下中のぷよを1段下げるとともに、コンピュータ側のぷよの操作をおこなっています。コンピュータ側がやっていることはいわゆるカエル積みです。なにも考えずぷよを両側に積み上げているだけです。これでもたまに高連鎖がおきることがあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class PuyoGame { bool isCpuMoving = false; private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { if (!isPlaying) return; Task.Run(async () => { await Fields[0].FallOne(); if (!isCpuMoving) { isCpuMoving = true; await Fields[1].AiAction(); isCpuMoving = 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 30 31 |
public class Field { int _count = 0; public async Task AiAction() { _count++; if (_count % 2 == 0) { Rotate(true); await Task.Delay(100); } while (true) { bool ret; // 右側の列が空いているなら右に移動できるだけ移動する // できない場合は左に移動できるだけ移動してから落とす if ( _fieldPuyos[INIT_MAIN_Y + 6, INIT_MAIN_X].Type == PuyoType.None || _fieldPuyos[INIT_MAIN_Y + 4, INIT_MAIN_X + 1].Type == PuyoType.None ) ret = MoveRight(); else ret = MoveLeft(); await Task.Delay(100); if (!ret) break; } await FallAll(); } } |