[C#] マインスイーパーの実装と考え方

C#

マインスイーパーの実装

C# でマインスイーパーを実装してみます。今回は、実際の実装コードのほかに、どのように考えてプログラムを実装していくか、という点も含めて解説もしていこうと思います。

どのようなインターフェースにするか考えてみる

マインスイーパーをプログラムで実装するにあたって、どのようなインターフェース(機能の使い方)にするべきか考えてみましょう。

これは「マインスイーパー」という機能を持つクラスがあったとして、使う側としてはどのようなものであれば扱いやすいか、ということを考えることになります。まずは、マインスイーパーにおいて、変更したいパラメータは何か、ということから考えてみましょう。

すぐに思いつくのは、フィールドのサイズと地雷の数ですね。

これらの値は、ゲームの難易度に関わるもので、さまざまに変更したいということが予想されます。この値はセットで渡せるほうが便利だと考えると、こういったマインスイーパーに関する設定をまとめるクラスを作成し、この設定クラスを渡して一つのゲームが作成できると良さそうです。

具体的には以下のようなコードになるでしょうか。

var config = new Minesweeper.Configuration();
config.FieldWidth = 24;
config.FieldHeight = 24;
config.MinesRate = 0.1;
var minesweeper = Minesweeper.CreateGame(config);

設定クラスを Minesweeper.CreateGame という関数に渡すと、Minesweeper クラスのインスタンスが作成できる、というコードです。このようなコードであれば、直感的で理解しやすいコードとなりそうですね。

公開すべき機能を考える

Minesweeper というクラスを作成するにあたって、公開するべき機能(関数 / メソッド)はどのようなものでしょうか。必ず必要になるのは、指定された座標のフィールドを開く機能ですね。

以下のような呼び出しがされるイメージです。

minesweeper.Open(1, 1);

このコードが呼び出された時に、内部の状態が更新されていれば、ゲームを進行させることができそうです。

また、この呼び出しを行ったときに、呼び出した側が知りたいのは、この呼び出しによって、ゲームが終了したか(指定された座標に地雷があったか)という点になります。これは単純に Open の返り値として、真偽値型(bool)を返すということで良さそうです。

もう一つ必要な点なのは、外部からフィールドの状態を知ることですね。状態さえ取得できれば、その状態に応じて描画を変更することで、使う側次第で色々な見た目のゲーム画面が作成できそうです。

以下はコンソール画面にフィールドの状態を表示するコードのイメージです。

for (int y = 0; y < config.FieldHeight; ++y)
{
    for (int x = 0; x < config.FieldWidth; ++x)
    {
        System.Console.Write(FieldStateTable[minesweeper.GetFieldState(x, y)]);
    }
    System.Console.WriteLine();
}

だんだんと実装するべき機能が見えてきましたね。それでは、Minesweeper クラスの中身を実装していきましょう。

マインスイーパーに必要な要素を考える

マインスイーパーは、四角形のフィールドを持つゲームです。このフィールドについては、二次元の配列で表現することができそうですね。

次に配列の型を考える必要がありますが、これはフィールドの状態がいくつあるかによって変わってきます。次のリストでフィールドの状態を一覧にしました。

  • 地雷がなく、フィールドが開かれていない状態
  • 地雷がなく、フィールドが開かれている状態
  • 地雷があって、フィールドが開かれていない状態
  • 地雷があり、フィールドが開かれている状態
  • 地雷があるフィールドにフラッグを立てている状態
  • 地雷がないフィールドにフラッグを立てている状態
  • 周囲に地雷があることを数値で表示している状態( 1 ~ 8 )

このように6種+8種で合わせて、14種の状態が必要そうです。14種類であれば、標準的な int 型の二次元配列で十分に事足りそうですね。

フィールドの状態管理を簡単にする方法を考える

14種の状態があるフィールドを実装する、ということでも、もちろん問題はありませんが、プログラムの基本的な考え方で重要なものとして「できる限り問題を簡単にする」という点があります。

状態の種類というのは、多ければ多いほど問題が複雑になってしまいます。今回、マインスイーパーを実装をしていく場合でも、できる限りフィールドの種類を減らしたいところです。リストを眺めながら考えてみると、組み合わせによって状態が冗長になっていることに気づくと思います。つまり

  • 地雷があるか/ないか
  • 開かれている状態か/閉じている状態か
  • フラッグが立てられているか/立てられていないか

という二種類で表現できる状態が組み合わさることで、状態の数が増えてしまっているということですね。これら二種類の状態は重複する可能性があるものですので、一つの数値型で表現しようとすると、組み合わせ一つ一つの状態に値を割り当てなくてはならず、どうしても状態の種類が増えてしまうのです。

これを解消する方法はいくつか存在します。一つはフィールドを複数持つこと、または、フィールドの状態を数値型ではなく、クラス(構造体)など複数の状態を持てる型にすること、もしくは、ビットフラグを用いて状態を管理することです。

今回は、一番考えることの少ないフィールドを複数持つ方法で種類を減らしてみましょう。まずは、地雷があるかないかだけを判定するフィールドを作ってみます。こちらの状態は二種類しかありませんので、真偽値型(bool)で良さそうですね。

/// <summary>
/// フィールド中の地雷を表す配列
/// </summary>
private bool[,] FieldMineLayer;

同様に、開いているか/開いていないかの状態を持つフィールド、フラッグが立てられているか/立てられていないかの状態を持つフィールドを用意していくことも行えば、さらに状態数を減らしていくことができるでしょう。

今回は、地雷の存在判定のみを別のフィールドで行うのみとします。この先は、読者の皆さんそれぞれで実装が簡単になる方法を試してみてください。

/// <summary>
/// フィールドの状態
/// </summary>
public enum FieldState
{
    /// <summary>
    /// 選択なし
    /// </summary>
    Unopen,

    /// <summary>
    /// 選択済み地雷なし
    /// </summary>
    OpenedEmptyMine,

    /// <summary>
    /// 選択済み地雷が周囲に存在(1)
    /// </summary>
    Opened1Mine,

    /// <summary>
    /// 選択済み地雷が周囲に存在(2)
    /// </summary>
    Opened2Mine,

    /// <summary>
    /// 選択済み地雷が周囲に存在(3)
    /// </summary>
    Opened3Mine,

    /// <summary>
    /// 選択済み地雷が周囲に存在(4)
    /// </summary>
    Opened4Mine,

    /// <summary>
    /// 選択済み地雷が周囲に存在(5)
    /// </summary>
    Opened5Mine,

    /// <summary>
    /// 選択済み地雷が周囲に存在(6)
    /// </summary>
    Opened6Mine,

    /// <summary>
    /// 選択済み地雷が周囲に存在(7)
    /// </summary>
    Opened7Mine,

    /// <summary>
    /// 選択済み地雷が周囲に存在(8)
    /// </summary>
    Opened8Mine,

    /// <summary>
    /// フラッグが立てられているか
    /// </summary>
    Flag,

    /// <summary>
    /// 選択済み地雷が存在
    /// </summary>
    OpenedMine,
}

/// <summary>
/// フィールドの状態を表す配列
/// </summary>
private FieldState[,] FieldStateLayer;

/// <summary>
/// フィールド中の地雷を表す配列
/// </summary>
private bool[,] FieldMineLayer;

Open 機能を実装する

では、公開すべき機能を考える 項目で示した Open を実装していきましょう。マインスイーパーの実装においては、ここの実装がキモであり、すべてであると言っても過言ではありません。とはいえ、実装する内容はシンプルです。

まずは、与えられた引数が示す座標のフィールドの状態を更新することを考えてみます。FieldMineLayer は地雷があるかどうかを保持している配列ですので、ここに引数で与えられた座標を渡して、地雷があるかを判定します。 この配列の中身が true だった場合、フィールドの状態を「開き済み&地雷」という状態に変更します。それが、以下のコードです。

/// <summary>
/// フィールドを開く
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public bool Open(int x, int y)
{
    if (FieldMineLayer[y, x])
    {
        FieldStateLayer[y, x] = FieldState.OpenedMine;
        return true;
    }
    return false;
}

では、指定された座標に地雷がなかった場合はどうなるでしょうか。その場合、周囲の座標を調べて周りにいくつの地雷があるかを数える、また、地雷がなければ、周囲のマスを連鎖的に開いていく必要があります。こちらの処理を else 節に書いていきましょう。

まずは、周囲のフィールドの地雷の数を数えるコードを記述しました。offsets は周囲の8マスを調べるためのオフセット配列です。このオフセットの座標と引数で受け取った座標を足し合わせれば、周囲のマスを調べることができます。

地雷数を数え終われば、フィールドの状態をそれぞれの地雷数に合わせた状態に変更すれば OK です。

/// <summary>
/// フィールドを開く
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public bool Open(int x, int y)
{
    if (CheckMine(x, y))
    {
        FieldStateLayer[y, x] = FieldState.OpenedMine;
        return true;
    }
    else
    {
        var offsets = new int[,] 
        {
            { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 },
            { 1, 1 }, { 1, -1 }, { -1, -1 }, { -1, 1 },
        };

        int mine = 0;
        for (int index = 0; index < offsets.GetLength(0); ++index)
        {
            if (CheckMine(x + offsets[index, 0], y + offsets[index, 1]))
            {
                ++mine;
            }
        }

        FieldStateLayer[y, x] = FieldState.OpenedEmptyMine + mine;
    }

    return false;
}

ただし、オフセットの座標を足すことによってマイナス座標など、配列外にアクセスする可能性が出てきます。専用の判定処理 CheckMine を作って、配列外へのアクセスが発生しないようにしておきました。地雷があるかの判定自体には、もちろん FieldMineLayer の配列を使用しています。

/// <summary>
/// 地雷があるかを判定
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public bool CheckMine(int x, int y)
{
    if (x < 0 || y < 0 || Config.FieldWidth <= x || Config.FieldHeight <= y) return false;

    return FieldMineLayer[y, x];
}

次に、地雷の数が0だった場合に、周囲のフィールドを開いていく処理を考えてみましょう。

フィールドを開くという処理は、Open でやっている処理に他なりませんので、今まで実装してきた Open がそのまま使えそうですね。また、周囲のフィールドを順番に辿る処理は、周囲の地雷数をカウントするのに使用した offsets 配列がそのまま使用できそうです。

/// <summary>
/// フィールドを開く
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public bool Open(int x, int y)
{
    if (FieldMineLayer[y, x])
    {
        FieldStateLayer[y, x] = FieldState.OpenedMine;
        return true;
    }
    else
    {
        var offsets = new int[,] 
        {
            { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 },
            { 1, 1 }, { 1, -1 }, { -1, -1 }, { -1, 1 },
        };

        int mine = 0;
        for (int index = 0; index < offsets.GetLength(0); ++index)
        {
            if (FieldMineLayer[x + offsets[index, 0], y + offsets[index, 1]])
            {
                ++mine;
            }
        }

        if (mine == 0)
        {
            FieldStateLayer[y, x] = FieldState.OpenedEmptyMine;

            for (int index = 0; index < offsets.GetLength(0); ++index)
            {
                if (CanOpen(x + offsets[index, 0], y + offsets[index, 1]))
                {
                    Open(x + offsets[index, 0], y + offsets[index, 1]);
                }
            }
        }
        else
        {
            FieldStateLayer[y, x] = FieldState.OpenedEmptyMine + mine;
        }
    }

    return false;
}

CanOpen は指定された座標のフィールドが開けるかどうかを判定するもので、指定された座標が地雷ではなく、かつ、フィールドが開いていない状態だった場合に true を返します。

/// <summary>
/// 開けることができるかを判定
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
private bool CanOpen(int x, int y)
{
    if (x < 0 || y < 0 || Config.FieldWidth <= x || Config.FieldHeight <= y) return false;

    return !FieldMineLayer[y, x] && FieldStateLayer[y, x] == FieldState.Unopen;
}

こちらでも、配列外のチェックは必要ですので、いれておきましょう。

以上で、マインスイーパーのコア機能の実装完了です。簡単でしたね!

UI や入力系統を実装していく

ここまで実装できれば、あとはフィールドの状態を取得して画面に表示し、ユーザーの入力を受け付ける実装などを追加すれば、ゲームとして遊ぶことができます。今回の実装では、画面描画に関する処理は実装していない(描画処理と分割できている)状態ですので、描画部分については柔軟に拡張することが可能です。

今回は簡単に、文字列の表示だけを使って、コンソール画面で遊べるように対応をしてみました。

ソースコード

今回書いたソースコードの全文は以下にあります。ライセンスは Unlicense として公開していますので、どなたでも自由に改変・再配布が可能です。

コメント

タイトルとURLをコピーしました