ASP.NET Coreで対コンピュータ対戦できるぷよぷよをつくります。
Contents
「ぷよぷよ」(Puyo Puyo)とは?
「ぷよぷよ」(Puyo Puyo)は落ち物パズルゲームのひとつです。縦12マス×横6マスのフィールドに2つ1組で上から落下してくるぷよを置いていきます。ぷよが4個以上くっつくと消滅します。ぷよが消滅することでその上にあるぷよが落下し、連鎖が起きることがあります。連鎖をつくって高得点を狙うのがこのゲームの目的です。
以前、ぷよぷよに似たゲームを作成しましたが、これは対戦型のゲームではなくお一人様用でした。今回は対コンピュータ戦ができるものとしてつくります。
主な仕様
PCで操作する場合は左右の移動は←キーと→ キー、急速落下は↓キー、回転は左がZキー、右はXキーとします。
ではさっそく作成することにします。名前空間はPuyoとします。C#で書かれたソースはPages\Puyoに置きます。それからNET 6.0をエックスサーバーにインストールするで示している手順が完了していることを前提としています。
PuyoクラスとFallingPuyoクラスの定義
ぷよには種類があります。そこで列挙体を定義します。ぷよが配置されていないときはPuyoType.Noneです。ぷよは4種類とし、それとはべつに「おじゃまぷよ」をつくります。
「おじゃまぷよ」は通常のぷよと異なり4つ以上くっついても消滅しません。おじゃまぷよを消すためには通常の色ぷよを同色4つ以上つなげて消滅させなければなりません。そうすることで消滅する色ぷよの上下左右に隣接するおじゃまぷよも一緒に消すことができます。
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace Puyo { public enum PuyoType { None, Type1, Type2, Type3, Type4, Ojama = -1 } } |
フィールドは縦12マス×横6マスなので二次元配列を定義してそこに配置します。最初にフィールド上に固定されているPuyoクラスを示します。
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 |
namespace Puyo { public class Puyo { public Puyo() { Type = PuyoType.None; Rensa = 0; } public Puyo(PuyoType type) { Type = type; Rensa = 0; } public PuyoType Type { get; private set; } public int Rensa { get; set; } } } |
固定されていない落下中のぷよの状態を管理するためのFallingPuyoクラスを定義します。Cloneメソッドは同じプロパティをもつ別のオブジェクトを返します。
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 |
namespace Puyo { public class FallingPuyo { public FallingPuyo(PuyoType type, int initCol, int initRow) { Col = initCol; Row = initRow; Type = type; } public PuyoType Type { get; private set; } public int Row { get; set; } public int Col { get; set; } public FallingPuyo Clone() { return new FallingPuyo(Type, Col, Row); } } } |
Fieldクラスの定義
これまでMicrosoft.AspNetCore.SignalR.Hubクラスを継承して、そのなかに静的なGameクラスのリストを生成していましたが、対戦型のぷよぷよではフィールドが自分と相手の2つ必要です。そこでGameクラス(仮称)のなかで使われるオブジェクトを生成するためのFieldクラスを先に定義します。
1 2 3 4 5 6 |
namespace Puyo { public class Field { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class 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 |
public class Field { public const int COL_MAX = 6; public const int ROW_MAX = 12 + 1; // 新しいプヨがでてくる位置と種類 const int INIT_MAIN_X = 2; const int INIT_MAIN_Y = 1; const int INIT_SUB_X = 2; const int INIT_SUB_Y = 0; readonly ReadOnlyCollection<int> REASA_BONUS = Array.AsReadOnly<int>( new int[] { 0, 0, 8, 16, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480, 512, } ); readonly ReadOnlyCollection<int> JOIN_COUNT_BONUS = Array.AsReadOnly<int>( new int[] { 0, 0, 0, 0, 0, 2, 3, 4, 5, 6, 7, 10, 10, } ); readonly ReadOnlyCollection<int> COLOR_COUNT_BONUS = Array.AsReadOnly<int>( new int[] { 0, 0, 3, 6, 12, 24, 24, } ); } |
コンストラクタの定義
ここでは二次元配列を初期化してなにもないぷよで埋め尽くしています。またIsPlayerプロパティはプレイヤーであればtrue、コンピュータであるならfalseです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Field { Puyo[,] _fieldPuyos = new Puyo[ROW_MAX, COL_MAX]; public Field(bool isPlayer) { IsPlayer = isPlayer; for (int row = 0; row < ROW_MAX; row++) { for (int col = 0; col < COL_MAX; col++) _fieldPuyos[row, col] = new Puyo(PuyoType.None); } } public bool IsPlayer { get; } } |
更新処理
データが更新されたときは他のクラスにそれを伝えることができるようにします。
Updateメソッドを呼び出すことでUpdatedイベントを発生させ、データが更新されたことを通知できるようにします。イベントハンドラの引数はすでにフィールド上に固定されているぷよと落下中のぷよ、そして落下中のおじゃまぷよです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Field { // 落下中のおじゃまぷよ List<FallingPuyo> _fallingOjamaPuyos = new List<FallingPuyo>(); // つぎ以降に落下するおじゃまぷよの個数 List<int> _nextOjamaPuyoCounts = new List<int>(); // 落下中の色ぷよ(_fallingPuyoMainを軸に回転する) FallingPuyo? _fallingPuyoMain = null; FallingPuyo? _fallingPuyoSub = null; public delegate void UpdatedHandler( Puyo[,] fixedPuyos, FallingPuyo? mainPuyo, FallingPuyo? subPuyo, List<FallingPuyo> ojamaPuyos); public event UpdatedHandler? Updated; public void Update() { Updated?.Invoke(_fieldPuyos, _fallingPuyoMain, _fallingPuyoSub, _fallingOjamaPuyos); } } |
おじゃまぷよを降らせる
落下中の組ぷよが固定されたら新しい組ぷよを落下させるのですが、そのときにおじゃまぷよを降らせる必要がある場合はさきにおじゃまぷよを落下させます。30個以上のおじゃまぷよを降らせる場合は2回にわけます。その処理を示します。
おじゃまぷよの位置ですが、6個にみたない場合は6列のどれかを乱数で選んでその列におじゃまぷよを仮置きします。それ以上の場合は1段6個で端数は左の列から配置します。そのあと仮置きしたおじゃまぷよがすべて最上段より上になるように平行移動させます。この位置がおじゃまぷよの初期位置となります。
初期位置が決まったらおじゃまぷよを0.05秒おきに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 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 |
public class Field { static Random _random = new Random(); async Task FallOjamaPuyo(int ojamaPuyoCount) { if (COL_MAX * ROW_MAX < ojamaPuyoCount) ojamaPuyoCount = COL_MAX * ROW_MAX; // 6個にみたない場合は6列あるフィールド(ただしその列の最上段にはぷよが存在しない)の // どれかを乱数で選んでその位置に降らせる List<int> cols = new List<int>(); for (int i = 0; i < COL_MAX; i++) { if (_fieldPuyos[1, i].Type == PuyoType.None) cols.Add(i); } // おじゃまぷよが置ける場所に仮置きする。 if (ojamaPuyoCount < cols.Count) { // おじゃまぷよが6個にみたない場合 for (int k = 0; k < ojamaPuyoCount; k++) { int r = _random.Next(cols.Count); int value = cols[r]; cols.RemoveAt(r); _fallingOjamaPuyos.Add(new FallingPuyo(PuyoType.Ojama, value, 0)); } } else { int setPuyo = 0; for (int row = ROW_MAX - 1; row > 0; row--) { for (int col = 0; col < COL_MAX; col++) { if (_fieldPuyos[row, col].Type == PuyoType.None) { _fallingOjamaPuyos.Add(new FallingPuyo(PuyoType.Ojama, col, row)); setPuyo++; if (setPuyo >= ojamaPuyoCount) break; } } if (setPuyo >= ojamaPuyoCount) break; } } // 仮置きしたおじゃまぷよがすべて最上段より上になるように平行移動させる // この位置がおじゃまぷよの初期位置 int max = _fallingOjamaPuyos.Max(_ => _.Row); foreach (FallingPuyo puyo in _fallingOjamaPuyos) puyo.Row -= max; _fallingOjamaPuyos = _fallingOjamaPuyos.OrderByDescending(_ => _.Row).ToList(); // 初期位置からおじゃまぷよを1段ずつ落下させる while (true) { bool isFalled = false; foreach (FallingPuyo puyo in _fallingOjamaPuyos) { if (puyo.Row + 1 >= 0) { if (puyo.Row + 1 >= ROW_MAX) _fieldPuyos[puyo.Row, puyo.Col] = new Puyo(PuyoType.Ojama); else if (_fieldPuyos[puyo.Row + 1, puyo.Col].Type == PuyoType.None) { puyo.Row = puyo.Row + 1; isFalled = true; } else _fieldPuyos[puyo.Row, puyo.Col] = new Puyo(PuyoType.Ojama); } else { puyo.Row = puyo.Row + 1; isFalled = true; } } await Task.Delay(50); Update(); if (!isFalled) break; } // すべてのおじゃまぷよの落下処理が完了したのでリストのなかを空にする _fallingOjamaPuyos.Clear(); } } |
前述の引数ありの同名メソッドを呼び出しておじゃまぷよを降らせる処理を示します。降らせるおじゃまぷよの数は_nextOjamaPuyoCountsのなかに格納されています。
30個以上のおじゃまぷよを降らせる場合は2回にわけたいので、_nextOjamaPuyoCountsの最初の要素が30より大きい場合は引数を30にして引数ありのFallOjamaPuyoメソッドを呼び出して最初の要素の値を差し引きます。30以下の場合はその値を引数にしてFallOjamaPuyoメソッドを呼び出したあと_nextOjamaPuyoCountsの最初の要素を削除します。
おじゃまぷよが降るタイミングで効果音を鳴らしたい(個数で音を変えたい)のでOjamaFalledイベントを定義しています。
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 Field { public delegate void OjamaFalledHandler(bool isPlayer, int count); public event OjamaFalledHandler? OjamaFalled; async Task FallOjamaPuyo() { if (_nextOjamaPuyoCounts.Count > 0) { int ojamaCount = _nextOjamaPuyoCounts[0]; if (ojamaCount > 0) OjamaFalled?.Invoke(IsPlayer, ojamaCount); if (ojamaCount > 30) { await FallOjamaPuyo(30); _nextOjamaPuyoCounts[0] -= 30; } else { _nextOjamaPuyoCounts.RemoveAt(0); if (ojamaCount > 0) await FallOjamaPuyo(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 |
public class Field { public async Task SetNewPuyo() { // ゲームオーバー判定 if (!IsSpace(INIT_MAIN_X, INIT_MAIN_Y)) { _isGameOvered = true; GameOvered?.Invoke(_fieldPuyos, _fallingPuyoMain, _fallingPuyoSub, _fallingOjamaPuyos); Update(); return; } // おじゃまぷよが必要なら降らせる await FallOjamaPuyo(); // 次の組ぷよを_nextsから取り出して_fallingPuyoMainと_fallingPuyoSubに格納する // _nextsの残り要素数が0のときや少ないときは10回分を作成する PuyoType[] puyoTypes = { PuyoType.Type1, PuyoType.Type2, PuyoType.Type3, PuyoType.Type4, }; if (_nexts.Count < 10) { for (int i = 0; i < 5; i++) { int r = _random.Next(4); _nexts.Add(new FallingPuyo(puyoTypes[r], INIT_MAIN_X, INIT_MAIN_Y)); r = _random.Next(4); _nexts.Add(new FallingPuyo(puyoTypes[r], INIT_SUB_X, INIT_SUB_Y)); } } FallingPuyo main = _nexts[0]; FallingPuyo sub = _nexts[1]; _nexts.RemoveAt(1); _nexts.RemoveAt(0); SendNext?.Invoke(main, sub, _nexts[0], _nexts[1], _nexts[2], _nexts[3]); _fallingPuyoMain = main; _fallingPuyoSub = sub; Update(); } } |
ゲーム開始時の処理
ゲーム開始時や次のステージが開始されるまえのフィールドの初期化の処理を示します。
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 Field { public async Task Init() { for (int row = 0; row < ROW_MAX; row++) { for (int col = 0; col < COL_MAX; col++) { Puyo puyo = new Puyo(PuyoType.None); _fieldPuyos[row, col] = puyo; } } _nexts.Clear(); _fallingOjamaPuyos.Clear(); _nextOjamaPuyoCounts.Clear(); await SetNewPuyo(); Update(); _isGameOvered = false; } } |
移動の処理
左右の移動の処理を示します。
落下中の色ぷよが壁にめり込んでしまう場合は移動できません。またすでにゲームオーバーになっている場合や_isAllowedMoveフラグがfalseの場合(急速落下中やステージとステージの間)は移動処理をおこないません。移動可能な場合は_fallingPuyoMainと_fallingPuyoSubのColプロパティを増減させます。
移動処理が行なわれた場合はtrueを返します。移動不能の場合は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 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 |
public class Field { bool _isAllowedMove = true; public bool MoveLeft() { if (!_isAllowedMove || _isGameOvered) return false; if (_fallingPuyoMain == null || _fallingPuyoSub == null) return false; int newMainCol = _fallingPuyoMain.Col - 1; int newSubCol = _fallingPuyoSub.Col - 1; int newMainRow = _fallingPuyoMain.Row; int newSubRow = _fallingPuyoSub.Row; if (newMainRow < 0) newMainRow = 0; if (newSubRow < 0) newSubRow = 0; if ( newMainCol < COL_MAX && newMainCol >= 0 && newSubCol < COL_MAX && newSubCol >= 0 && _fieldPuyos[newMainRow, newMainCol].Type == PuyoType.None && _fieldPuyos[newSubRow, newSubCol].Type == PuyoType.None ) { _fallingPuyoMain.Col = newMainCol; _fallingPuyoSub.Col = newSubCol; Update(); return true; } else return false; } public bool MoveRight() { if (!_isAllowedMove || _isGameOvered) return false; if (_fallingPuyoMain == null || _fallingPuyoSub == null) return false; int newMainCol = _fallingPuyoMain.Col + 1; int newSubCol = _fallingPuyoSub.Col + 1; int newMainRow = _fallingPuyoMain.Row; int newSubRow = _fallingPuyoSub.Row; if (newMainRow < 0) newMainRow = 0; if (newSubRow < 0) newSubRow = 0; if ( newMainCol < COL_MAX && newMainCol >= 0 && newSubCol < COL_MAX && newSubCol >= 0 && _fieldPuyos[newMainRow, newMainCol].Type == PuyoType.None && _fieldPuyos[newSubRow, newSubCol].Type == PuyoType.None ) { _fallingPuyoMain.Col = newMainCol; _fallingPuyoSub.Col = newSubCol; Update(); return true; } else return false; } } |
組ぷよを落下させる
組ぷよの片方(もう片方はこれを軸に周囲を回転する)を落下させる処理を示します。
すでに固定されたぷよが存在する場合でないなら落下させると判断します。この場合は_fallingPuyoMainのRowプロパティを1増加させてtrueを返します。できない場合は落下中のぷよをフィールド上に固定し、falseを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Field { bool FallMain() { if (_fallingPuyoMain == null) return false; int newMainRow = _fallingPuyoMain.Row + 1; if ( newMainRow >= ROW_MAX || _fieldPuyos[newMainRow, _fallingPuyoMain.Col].Type != PuyoType.None ) { _fieldPuyos[_fallingPuyoMain.Row, _fallingPuyoMain.Col] = new Puyo(_fallingPuyoMain.Type); return false; } _fallingPuyoMain.Row = newMainRow; return true; } } |
組ぷよのもう片方(前述のぷよを軸に周囲を回転する)を落下させる処理を示します。
すでに固定されたぷよが存在する場合でないなら落下させると判断します。この場合は_fallingPuyoSubのRowプロパティを1増加させてtrueを返します。できない場合は落下中のぷよをフィールド上に固定し、falseを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Field { bool FallSub() { if (_fallingPuyoSub == null) return false; int newSubRow = _fallingPuyoSub.Row + 1; if ( newSubRow >= ROW_MAX || _fieldPuyos[newSubRow, _fallingPuyoSub.Col].Type != PuyoType.None ) { _fieldPuyos[_fallingPuyoSub.Row, _fallingPuyoSub.Col] = new Puyo(_fallingPuyoSub.Type); return false; } _fallingPuyoSub.Row = newSubRow; return true; } } |
組ぷよをセットで1段だけ落下させる処理を示します。
組ぷよが存在しない場合はなにもしません。前述のFallMainメソッドとFallSubメソッドでぷよを下に移動させてみます。このとき組ぷよが縦に並んでいる場合は両方の位置関係に注意します。下にあるほうを先に移動させます。
両方ともfalseが返される場合は組ぷよのふたつが同時に着地したことを意味します。この場合はFixPuyoメソッド(後述)でフィールド上に落下していた組ぷよを固定します。
片方だけfalseを返した場合は片方だけ着地していることを意味します。この場合は組ぷよはちぎれて他方が着地するまで落下し続けます。この場合じゃ着地していないぷよが着地するまで(FallMainメソッドまたはFallSubメソッドがfalseを返すまで)呼び出し続けます。そしてFixPuyoメソッドを呼び出してフィールド上にぷよを固定します。
ぷよがちぎれて落下しているときにキー操作をすると動作がおかしくなるので、処理中は_isAllowedMoveフラグを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 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 |
public class Field { async Task<bool> Fall() { if (_fallingPuyoMain == null || _fallingPuyoSub == null) return false; bool isMainFixed; bool isSubFixed; if (_fallingPuyoMain.Row > _fallingPuyoSub.Row) { isMainFixed = !FallMain(); isSubFixed = !FallSub(); } else { isSubFixed = !FallSub(); isMainFixed = !FallMain(); } Update(); if (isMainFixed && isSubFixed) { await FixPuyo(); return false; } else if (isMainFixed && !isSubFixed) { _isAllowedMove = false; while (true) { await Task.Delay(50); if (!FallSub()) break; Update(); } await FixPuyo(); _isAllowedMove = true; return false; } else if (!isMainFixed && isSubFixed) { _isAllowedMove = false; while (true) { await Task.Delay(50); if (!FallMain()) break; Update(); } await FixPuyo(); _isAllowedMove = true; return false; } return true; } } |
これはクラスの外部からFallメソッドを呼び出せるようにしたものです。ゲームオーバー時や_isAllowedMoveフラグがfalseの場合以外は上記のFallメソッドを呼び出します。
1 2 3 4 5 6 7 8 9 |
public class Field { public async Task FallOne() { if (!_isAllowedMove || _isGameOvered) return; await Fall(); } } |
FallAllメソッドは急速落下です。組ぷよを落下できる位置まで一気に落下させます。この場合も処理中に他の移動操作ができてしまうと困るので_isAllowedMoveフラグで制御しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Field { public async Task FallAll() { if (!_isAllowedMove || _isGameOvered) return; _isAllowedMove = false; while (await Fall()) await Task.Delay(50); _isAllowedMove = true; } } |
組ぷよを回転させる
組ぷよを回転させる処理を示します。_fallingPuyoMainと_fallingPuyoSubの位置関係から回転処理をしたときの_fallingPuyoSubの位置を取得して、その位置に移動可能であれば回転処理をしています。組ぷよが存在しない場合や_fallingPuyoSubの移動先に固定されたぷよが存在する場合、やフィールドの外にでてしまう場合、ゲームオーバー時や_isAllowedMoveがfalseのは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 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 |
public class Field { public bool Rotate(bool isRight) { if (!_isAllowedMove || _isGameOvered) return false; if (_fallingPuyoMain == null || _fallingPuyoSub == null) return false; int newSubCol = 0; int newSubRow = 0; if (_fallingPuyoMain.Row > _fallingPuyoSub.Row) { if (isRight) newSubCol = _fallingPuyoSub.Col + 1; else newSubCol = _fallingPuyoSub.Col - 1; newSubRow = _fallingPuyoSub.Row + 1; } else if (_fallingPuyoMain.Col < _fallingPuyoSub.Col) { newSubCol = _fallingPuyoSub.Col - 1; if (isRight) newSubRow = _fallingPuyoSub.Row + 1; else newSubRow = _fallingPuyoSub.Row - 1; } else if (_fallingPuyoMain.Row < _fallingPuyoSub.Row) { if (isRight) newSubCol = _fallingPuyoSub.Col - 1; else newSubCol = _fallingPuyoSub.Col + 1; newSubRow = _fallingPuyoSub.Row - 1; } else if (_fallingPuyoMain.Col > _fallingPuyoSub.Col) { newSubCol = _fallingPuyoSub.Col + 1; if (isRight) newSubRow = _fallingPuyoSub.Row - 1; else newSubRow = _fallingPuyoSub.Row + 1; } int tempNewSubY = newSubRow; if (tempNewSubY < 0) tempNewSubY = 0; if ( newSubRow < ROW_MAX && newSubCol < COL_MAX && newSubCol >= 0 && _fieldPuyos[tempNewSubY, newSubCol].Type == PuyoType.None ) { _fallingPuyoSub.Col = newSubCol; _fallingPuyoSub.Row = newSubRow; Update(); return true; } else return false; } } |
組ぷよをフィールド上に固定する
組ぷよをフィールド上に固定したら4つ以上つながっている場合はぷよを消す処理と連鎖するときは連鎖する処理をおこなわなければなりません。最初に固定されたぷよが4つ以上つながっているかどうかを調べる処理を示します。
IsSpaceメソッドはそこはぷよが存在しない、または存在しないとみなせる部分かを調べるためのものです。その地点がPuyoType.NoneであったりRensaプロパティが1以上(これから消えようとしているぷよが存在する)の場合はtrueを返します。
Checkメソッドでは引数で渡された地点の上下左右の同じ色の部分を取得し、さらにそれと上下左右の同じ色の部分を取得する処理を繰り返しています。このとき一度調べた場所を何度も調査して無限ループにならないようにチェック済みの部分を二重チェックしないようにしています。またIsSpaceメソッドがtrueを返すPuyo(ぷよが存在しないか、すでに消去するぷよとしてマークされている地点)やTypeプロパティがPuyoType.OjamaのPuyoは対象外です。
取得されたPuyoの数が4つ以上であるならそれは消去されるぷよです。この場合はそのPuyoのRensaプロパティに_rensaCountをセットしてそのリストを返します。
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 |
public class Field { int _rensaCount = 1; bool IsSpace(int col, int row) { if (_fieldPuyos[row, col].Type == PuyoType.None) return true; if (_fieldPuyos[row, col].Rensa > 0) return true; return false; } List<Puyo> Check(int col, int row) { Puyo puyo = _fieldPuyos[row, col]; if (IsSpace(col, row) || puyo.Type == PuyoType.Ojama) return new List<Puyo>(); bool[,] fieldForCheck = new bool[ROW_MAX, COL_MAX]; fieldForCheck[row, col] = true; Queue<int> xs = new Queue<int>(); Queue<int> ys = new Queue<int>(); List<int> resultXs = new List<int>(); List<int> resultYs = new List<int>(); xs.Enqueue(col); ys.Enqueue(row); resultXs.Add(col); resultYs.Add(row); while (true) { int x0 = xs.Dequeue(); int y0 = ys.Dequeue(); int[] dxs = { 0, 0, 1, -1 }; int[] dys = { 1, -1, 0, 0 }; for (int i = 0; i < 4; i++) { int nextX = x0 + dxs[i]; int nextY = y0 + dys[i]; if (nextX < 0 || nextY < 0 || nextX >= COL_MAX || nextY >= ROW_MAX) continue; if (fieldForCheck[nextY, nextX]) continue; fieldForCheck[nextY, nextX] = true; if (_fieldPuyos[nextY, nextX].Type == puyo.Type) { xs.Enqueue(nextX); ys.Enqueue(nextY); resultXs.Add(nextX); resultYs.Add(nextY); } } if (xs.Count == 0) break; } if (resultXs.Count < 4) return new List<Puyo>(); // 4つ以上つながっていたら消す List<Puyo> ret = new List<Puyo>(); for (int k = 0; k < resultXs.Count; k++) { _fieldPuyos[resultYs[k], resultXs[k]].Rensa = _rensaCount; ret.Add(_fieldPuyos[resultYs[k], resultXs[k]]); } return ret; } } |
4つ以上同じ色でつながっているぷよを消す処理を示します。
DownByDeleteメソッドは各ぷよの下が空洞の場合、ぷよを1段下げる処理をおこないます。
前述のCheckメソッドで消えるPuyoを取得します。これが存在しない場合はなにもしないで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 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 |
public class Field { int _addScore = 0; // ぷよが固定されたことで追加される点数 public event EventHandler? Rensa; // 連鎖発生時のイベント(効果音を鳴らしたいだけ) bool DownByDelete() { bool isDowned = false; for (int row = ROW_MAX - 1; row > 0; row--) { for (int col = 0; col < COL_MAX; col++) { if (IsSpace(col, row) && !IsSpace(col, row - 1)) { Puyo puyo = _fieldPuyos[row - 1, col]; _fieldPuyos[row, col] = puyo; _fieldPuyos[row - 1, col] = new Puyo(PuyoType.None); isDowned = true; } } } return isDowned; } async Task<bool> CheckDelete() { // 消えるPuyoを格納しているリストを格納する List<List<Puyo>> deletingPuyos = new List<List<Puyo>>(); for (int row = 0; row < ROW_MAX; row++) { for (int col = 0; col < COL_MAX; col++) { if (_fieldPuyos[row, col].Type == PuyoType.Ojama) continue; List<Puyo> ret = Check(col, row); if (ret.Count > 0) { Update(); deletingPuyos.Add(ret); } } } // 消えるPuyoが存在しない場合はなにもしない if (deletingPuyos.Count == 0) return false; // 効果音を鳴らすためにイベントを発生させる Rensa?.Invoke(this, new EventArgs()); await Task.Delay(500); // 上下左右のおじゃまぷよ消去用の配列 int[] xs = { 0, 0, 1, -1 }; int[] ys = { 1, -1, 0, 0 }; // _fieldPuyos[row, col].Rensa > 0 の部分を探す for (int row = 0; row < ROW_MAX; row++) { for (int col = 0; col < COL_MAX; col++) { if (_fieldPuyos[row, col].Rensa > 0) { // 見つかったらその部分をPuyoType.Noneにする _fieldPuyos[row, col] = new Puyo(PuyoType.None); // 見つかったらその部分の上下左右のおじゃまぷよも同時に消す for (int i = 0; i < 4; i++) { int newCol = col + xs[i]; int newRow = row + ys[i]; if (newCol < 0 || newCol >= COL_MAX || newRow < 0 || newRow >= ROW_MAX) continue; if (_fieldPuyos[newRow, newCol].Type != PuyoType.Ojama) continue; _fieldPuyos[newRow, newCol] = new Puyo(PuyoType.None); } } } } Update(); // 下が空洞になることで宙に浮いているぷよを1段下げて空洞を詰める // これを処理が行なわれなくなるまで繰り返す while (true) { if (!DownByDelete()) break; Update(); await Task.Delay(100); } int totalCount = 0; // 消えたぷよの総和 int totalJoinCountBonus = 0; // 連結ボーナスの総和 List<PuyoType> puyoTypes = new List<PuyoType>(); foreach (List<Puyo> puyos in deletingPuyos) { puyoTypes.Add(puyos[0].Type); totalCount += puyos.Count; totalJoinCountBonus += JOIN_COUNT_BONUS[puyos.Count]; } int typeCount = puyoTypes.Distinct().ToList().Count; // ぷよの消えた数×10×(連鎖ボーナス+連結ボーナス+色数ボーナス) int score = totalCount * 10 * (REASA_BONUS[_rensaCount] + (totalJoinCountBonus) + COLOR_COUNT_BONUS[typeCount]); if (score == 0) score = totalCount * 10; // 括弧内が0の場合は1とする _addScore += score; return true; } } |
落下し終わった組ぷよをフィールド上に固定する処理を示します。
フィールド変数 _addScoreを0で、_rensaCountを1で初期化して、前述のCheckDeleteメソッドをfalseを返すまで呼び出し続けます。CheckDeleteメソッドが true を返す限り連鎖が進行していることを意味するので、trueを返した場合は_rensaCountをインクリメントしてCheckDeleteメソッドを呼び出す処理を繰り返します。処理がおわったら新しい組ぷよを落下させる処理をおこないます。
またこの処理が終了したときに_addScoreが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 |
public class Field { public delegate void ScoreChangedHandler(int addScore); public event ScoreChangedHandler? ScoreChanged; async Task FixPuyo() { if (_fallingPuyoMain == null || _fallingPuyoSub == null) return; // 4つ以上くっついたぷよを消去する処理中は他の操作をできなくする _isAllowedMove = false; // 落下中のぷよを見えないところに移動する _fallingPuyoMain.Row = -1; _fallingPuyoSub.Row = -1; // 接地直後の連鎖数は 1 である _rensaCount = 1; _addScore = 0; while (true) { // CheckDeleteメソッドが true を返す限り連鎖が進行していることを意味する bool ret = await CheckDelete(); if (!ret) break; _rensaCount++; } // 処理がおわったら新しい組ぷよを落下させる await SetNewPuyo(); _isAllowedMove = true; if(_addScore > 0) ScoreChanged?.Invoke(_addScore); } } |
敵側におじゃまぷよを送り込む
得点によっては敵側におじゃまぷよを送り込む処理が必要になります。送り込まれるおじゃまぷよの数は追加された点数によって決まります。計算の結果求められた数のおじゃまぷよが、敵側が現在落下させているぷよが固定されたあとに出現するぷよが固定されたときに降ってきます(1ターン分の猶予がある)。
そこで_nextOjamaPuyoCountsが空の場合は0を格納したあと、その個数を格納します。要素数が1の場合はその次に、要素数が2のときは最後の要素の値に加算します。要素数が3以上になることはないはずですが、その場合は3番目以降を要素数を2にして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 26 27 28 29 30 |
public class Field { public void SetOjamaPuyo(int puyoCount) { if (_nextOjamaPuyoCounts.Count == 0) { _nextOjamaPuyoCounts.Add(0); _nextOjamaPuyoCounts.Add(puyoCount); } else if (_nextOjamaPuyoCounts.Count == 1) { _nextOjamaPuyoCounts.Add(puyoCount); } else if (_nextOjamaPuyoCounts.Count == 2) { _nextOjamaPuyoCounts[1] += puyoCount; } else { int first = _nextOjamaPuyoCounts[0]; int second = _nextOjamaPuyoCounts[1]; for (int i = 2; i < _nextOjamaPuyoCounts.Count; i++) second += _nextOjamaPuyoCounts[i]; _nextOjamaPuyoCounts.Clear(); _nextOjamaPuyoCounts.Add(first); _nextOjamaPuyoCounts.Add(second); } } } |
点数が加算されたときに敵側からすでに送り込まれようとしているおじゃまぷよがある場合は相殺の処理がおこなわれます。その処理を示します。
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 |
public class Field { public int RemoveOjamaPuyo(int puyoCount) { if(_nextOjamaPuyoCounts.Count == 0) return puyoCount; if (_nextOjamaPuyoCounts[0] > puyoCount) { _nextOjamaPuyoCounts[0] -= puyoCount; puyoCount = 0; } else { puyoCount -= _nextOjamaPuyoCounts[0]; _nextOjamaPuyoCounts[0] = 0; } if (_nextOjamaPuyoCounts.Count == 1) return puyoCount; if (_nextOjamaPuyoCounts[1] > puyoCount) { _nextOjamaPuyoCounts[1] -= puyoCount; puyoCount = 0; } else { puyoCount -= _nextOjamaPuyoCounts[1]; _nextOjamaPuyoCounts[1] = 0; } if (_nextOjamaPuyoCounts.Count == 2) return puyoCount; // _nextOjamaPuyoCounts.Count が3以上になることはないはず return puyoCount; } } |
これから落下しようとするおじゃまぷよがある場合はその数を表示させます。その数を取得する処理を示します。
1 2 3 4 5 6 7 |
public class Field { public int GetYokokuCount() { return _nextOjamaPuyoCounts.Sum(); } } |