今回は対戦型のマインスイーパーをつくります。
元々のマインスイーパーは1980年代に発明された一人用のコンピュータゲームです。ゲーム画面は正方形のマスが敷き詰められた長方形のフィールドから構成されています。地雷の置かれているマスを開けないように、それ以外のすべてのマスを開ければ成功です。
地雷が置かれていないマスを開けたときは、隣接する8方向のマスのいずれかに地雷がある場合はその個数が表示されます。隣接するマスに地雷がない場合はその部分が自動的に開きます。
またプレイヤーは地雷の位置の推測を容易にするために、地雷が置かれていると思われるマスに旗を立てることができます。
Microsoft Windowsのものでは、
初級:9×9のマスに10個の地雷
中級:16×16のマスに40個の地雷
上級:30×16のマスに99個の地雷
があります。ところがWindows 8からは標準では搭載されず、ストアアプリからダウンロードしなければなりません。
ただこれから作ろうとしているものは対戦型なのでもっとフィールドを広くしようと考えています。32×32なんてのはどうでしょうか?地雷があると思われる場所には旗を立てることができますが、他のプレイヤーからはみることはできません。
それではさっそくつくってみましょう。
Positionクラスの定義
まず名前空間ですがMinesweeperGameとします。
最初に位置を格納するためのPositionクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
namespace MinesweeperGame { public class Position { public Position(int col, int row) { Column = col; Row = row; } public int Column { get; } public int Row { get; } } } |
次にゲームに関する情報、処理をするGameクラスを定義します。
Gameクラスの定義
1 2 3 4 5 6 |
namespace MinesweeperGame { public class Game { } } |
と書くべきところを名前空間を省略して
1 2 3 |
public class Game { } |
と書くことにします。
まずフィールド変数を示します。
最初の3つは見ての通りです。MapとIsMapOpenは二次元配列でMapの各要素は-1なら地雷、それ以外の部分はその周囲に存在する地雷の数です。IsMapOpenはすでに自分または他のプレイヤーによって開かれているかどうかを示すものです。
_randomは地雷の場所を決めるときに使う乱数生成用のものであり、PlayersはAspNetCore.SignalRで接続したときのConnectionIDとPlayerオブジェクトの辞書です。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Game { public const int ColMax = 32; public const int RowMax = 32; public const int MineCount = 240; public static int[,] Map = { { } }; public static bool[,] IsMapOpen = { { } }; static Random _random = new Random(); public static Dictionary<string, Player> Players = new Dictionary<string, Player>(); } |
初期化の処理を示します。ここでは32×32のセルにランダムに地雷をセットしてセルを空けたときに表示される数字をセットしています。
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 Game { public static void Init() { // 最初はMapの各要素を0に、IsMapOpenの各要素をfalseにする Map = new int[RowMax, ColMax]; IsMapOpen = new bool[RowMax, ColMax]; List<Position> positions = new List<Position>(); List<Position> mines = new List<Position>(); // 二次元配列をリストに変換、Positionにcol, rowを格納する for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) positions.Add(new Position(col, row)); } // リストのなかから地雷をセットするPositionをランダムに選ぶ for (int i = 0; i < MineCount; i++) { int r = _random.Next(positions.Count); mines.Add(positions[r]); positions.RemoveAt(r); } // 地雷をセットしたPositionの周囲に地雷の数をセットする int[] dx = { 0, 0, 1, -1, 1, -1, 1, -1 }; int[] dy = { 1, -1, 0, 0, 1, -1, -1, 1 }; foreach (Position mine in mines) { for (int k = 0; k < dx.Length; k++) { int ncol = mine.Column + dx[k]; int nrow = mine.Row + dy[k]; if (ncol < 0 || ncol >= ColMax || nrow < 0 || nrow >= RowMax) continue; Map[nrow, ncol] += 1; } } // 地雷がセットされているPositionには-1を代入する foreach (Position mine in mines) Map[mine.Row, mine.Column] = -1; } } |
各クライアントにセルを描画するための情報を送信しなければならないのですが、GetStringFromMapメソッドはその送信するカンマ区切りの文字列を生成します。
旗が立っているのであれば-3、すでに誰かが開いているセルであれば表示すべき数字、開かれていないのであれば-2とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Game { public static string GetStringFromMap(Player? player) { List<int> vs = new List<int>(); for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) { if (player != null && player.HasFlag(col, row)) // プレイヤーが旗を立てている vs.Add(-3); else if (IsMapOpen[row, col]) // プレイヤーによって開かれたセル vs.Add(Map[row, col]); else vs.Add(-2); // 開かれていないセル } } return String.Join(",", vs.ToArray()); } } |
GetUnopenedCellCountメソッドは地雷がセットされていないセルの数を返します。これが0になったらステージクリアとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Game { public static int GetUnopenedCellCount() { int open = 0; for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) { if (IsMapOpen[row, col]) open++; } } return RowMax * ColMax - open - MineCount; } } |
Playerクラスの定義
次にプレイヤーに関する処理をおこなうPlayerクラスを示します。
1 2 3 4 5 6 |
namespace MinesweeperGame { public class Player { } } |
と書くべきところを名前空間を省略して
1 2 3 |
public class Player { } |
と書くことにします。
コンストラクタとプロパティを示します。
ConnectionIdはAspNetCore.SignalRで接続したときに定まるIDです。
Nameはプレイヤーの名前で、空文字列のときは名無しさんになります。また長すぎる名前の場合は16文字に切り詰めています。それからスコアランキングをカンマ区切りのテキストファイルで管理するのでプレイヤー名のなかにカンマがある場合は別の文字に置き換えています。
Scoreは読んで字のごとしそのプレイヤーのスコアです。
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 |
public class Player { public Player(string connectionId, string name) { ConnectionId = connectionId; Name = name; } public string ConnectionId { get; } string _name = ""; public string Name { set { if (value == "") _name = "名無しさん"; else { string name = value.Length > 16 ? value.Substring(0, 16) : value; _name = name.Replace(",", "_"); } } get { return _name; } } public int Score { set; get; } } |
旗の管理
プレイヤーが地雷があるかもしれないと思った場所に旗を立てますが、これはそれ以外のプレイヤーに見られないようにします。そこでどこに旗を立てているかは各Playerオブジェクトで管理します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Player { List<Position> _flags = new List<Position>(); public List<Position> Flags { get { return _flags; } } // そのセルに立てられている旗をクリアする public void ClearFlag(int col, int row) { _flags = _flags.Where(f => (f.Column != col || f.Row != row)).ToList(); } // そのセルに旗は立てられているか? public bool HasFlag(int col, int row) { return _flags.Any(f => (f.Column == col && f.Row == row)); } } |