神経衰弱をつくる(前編)

こんにちは、きうちです。

前回の更新(シェルピンスキーのクリスマスツリー(後編))で、「一年やりました!」とか言っていたんですが、よく見たら私は昨年の1月はミニプログラムを投稿していませんでした。そのときは文章のみのネタでした。

というわけで、急遽、今月も何かミニプログラムをお届けしたいと思います!

それで、内容は何にしようかと考えていたんですが、ここはひとつ初心に返って(?)「神経衰弱」を作ってみたいと思います!

ルールについては・・・御覧の皆さまはご存じですよね。カードが何枚か伏せて並べられているので、それを2枚ずつめくって同じ番号(や種別)のカードをめくることができたらその2枚を獲得できるという。

トランプのカードなんかでよくやると思うのですが、ここではコンピュータゲームですのでちょっと趣向を変えて
「色」
にしてみようかと思います。

言語は例によってC#にします。Windowsフォームアプリとして作成します。
プロジェクト名はわかりやすく ShinkeiSuijaku にしようかな。

★ ★ ★

で、この手の「場に何かを並べる」系のものは二次元配列でやるのが楽ですね。

要素の型は通常は int でいいと思います。

たとえばトランプなら、3桁の数で表現できますね。

100の位をマーク、10の位と1の位を数字にしたら、

マーク...1=ハート、2=ダイヤ、3=スペード、4=クラブ

と定義するなら、ハートのエースは「101」、スペードの10は「310」などと表現できます。

・・・なんですが、今回は、あえて拡張性を持たせるためにクラスで要素を定義してみようかと思います。

こんな感じのクラス。

フォームとは別に用意します(が、フォームクラスの中に書いても動きます)。

class Card // 「場」の各要素(カード)を表現するクラス
{
    public int Number { get; set; } = 0; // 番号

    public bool Shown { get; set; } = false; // 表になっているか否か

    public Card(int number, bool shown) // コンストラクタ
    {
        Number = number;
        Shown = shown;
    }
}

「場」を保持するための配列変数はこれを要素に持つので、

private Card[,] _cells = new Card[13, 4]; // 「場」を表す配列変数

となりますね。

なんとなく要素数を13×4にしましたが、これは今回は

  • トランプ同様同じ番号を持つカードが4つ×13=52枚
  • 「場」は4行13列とする

としようと考えたためです。

初期処理はこんな風に書きます。

public Form1() // コンストラクタ
{
    InitializeComponent();

    Text = "神経衰弱"; // タイトル設定
    BackColor = Color.Black; // 背景を黒にする
    SetBounds(0, 0, 64 * 13 + 17, 88 * 4 + 40, BoundsSpecified.Size); // フォームのサイズを適当に設定する
    DoubleBuffered = true; // ちらつきを抑えるため、ダブルバッファにする

    StartNewGame(); // 新規ゲームを開始する
}

最後に StartNewGame というメソッドを呼んでいますが、これは「新規ゲーム開始」のための処理がもしかしたら長くなるかもしれないので、このように別メソッドにしています。

ひとまずはこんな感じかと思います。

private void StartNewGame() // 新規ゲームを開始する
{
    Arrange(); // カードを並べる
}

private void Arrange() // カードを並べる
{
    for (int x = 0; x < 13; x++) // 横13マスの繰り返し
    {
        for (int y = 0; y < 4; y++) // 縦4マスの繰り返し
        {
            int num = x + 1; // 番号はX座標に1を足したものにする
            _cells[x, y] = new Card(num, true); // 「場」の要素をセットする
        }
    }
}

Card のコンストラクタに渡す第2引数がtrueになっていますが、これはまだ作り始めの段階なので、表示の確認のために最初から表返っている状態にしておこうかな、ということでこうなっています。

描画処理はこんな感じ。

private void Form1_Paint(object? sender, PaintEventArgs e) // フォーム描画処理
{
    DrawCells(e.Graphics); // 「場」を描く
}

private void DrawCells(Graphics g) //「場」を描く
{
    for (int x = 0; x < 13; x++) // 横13マスの繰り返し
    {
        for (int y = 0; y < 4; y++) // 縦4マスの繰り返し
        {
            if (_cells[x, y] != null) // カードが存在する状態であれば
            {
                if (_cells[x, y].Shown) // 表の場合
                {
                    g.FillRectangle(Brushes.White, x * 64 + 1, y * 88 + 1, 62, 86); // 白い塗りつぶしの四角を描く
                    g.DrawString($"({x + 1},{y + 1})\n{_cells[x, y].Number}", new Font("MS ゴシック", 16), Brushes.Black, x * 64 + 2, y * 88 + 2); // 座標と番号を描く
                }
                else // 裏の場合
                {
                    g.FillRectangle(Brushes.Gray, x * 64 + 1, y * 88 + 1, 62, 86); // グレーの塗りつぶしの四角を描く
                }
            }
        }
    }
}

横64、縦88で考えていますので、描画時は X=0~12 を64倍、Y=0~3を88倍しています。

色は表が白、裏がグレーとします。

Form1_Paint はフォームデザイナでフォームのPaintイベントに紐づけるか、コンストラクタに次のように書いておきます。

Paint += Form1_Paint;

ここまでで、実行してみましょう。

・・・カードが奇麗に並びましたね!

★ ★ ★

しかし、このままでは「何がどこにあるか」わかりやすすぎますね。なので、シャッフルしましょう。
シャッフルの為の処理を次のように実装します。

private Random random = new Random(); // 乱数発生オブジェクト

private void Shuffle() // シャッフルする
{
    for (int i = 0; i < 1000; i++) // とりあえず1000回くらい混ぜる
    {
        int x1 = random.Next(0, 13); // 1箇所目のX座標を決める
        int y1 = random.Next(0, 4);  // 1箇所目のY座標を決める
        int x2 = random.Next(0, 13); // 2箇所目のX座標を決める
        int y2 = random.Next(0, 4);  // 2箇所目のY座標を決める

        Card card1 = _cells[x1, y1]; // 1箇所目のカードを取得
        Card card2 = _cells[x2, y2]; // 2箇所目のカードを取得
        _cells[x1, y1] = card2; // 1箇所目のカードを2箇所目の位置に置く
        _cells[x2, y2] = card1; // 2箇所目のカードを1箇所目の位置に置く
    }
}

これは「2つの個所をランダムに決めて、それらのカードを交換する」ということを繰り返しています。

続いて、このShuffleメソッドの呼びだしを StartNewGameメソッドのArrangeの呼び出しの次に追加します。

private void StartNewGame() // 新規ゲームを開始する
{
    Arrange(); // カードを並べる
    Shuffle(); // 【追加】カードをシャッフルする
    Refresh(); // 【追加】画面を再描画する(念のため)
}

この状態で実行します。

カードの並びがばらばらになりましたね!

★ ★ ★

あとは、

  • 初期状態ではカードが裏返っている
  • クリックすると表になる
  • 2枚表にしたらペアになっているか判定
    • ペアになっていたら除去
    • ペアになっていなければ裏返す

とすればいいですね。

「初期状態ではカードが裏返っている」は、ArrangeメソッドでCardのコンストラクタをtrueにしていましたから、これをfalseにすればそうなります。

private void Arrange() // カードを並べる
{
    for (int x = 0; x < 13; x++) // 横13マスの繰り返し
    {
        for (int y = 0; y < 4; y++) // 縦4マスの繰り返し
        {
            int num = x + 1; // 番号はX座標に1を足したものにする
            _cells[x, y] = new Card(num, false); // 「場」の要素をセットする【変更】第2引数は false にする
        }
    }
}

この状態で実行してみます。

全部裏返った状態(グレーで、番号が表示されていない)で表示されましたね!

「クリックすると表になる」はこんな感じです。

private void Form1_MouseClick(object? sender, MouseEventArgs e) // マウスクリック処理
{
    int x = e.X / 64; // クリックされたX座標を64で割る
    int y = e.Y / 88; // クリックされたY座標を88で割る

    if (x >= 0 && x < _cells.GetLength(0) && y >= 0 && y < _cells.GetLength(1)) // カードじゃないところをクリックしていないかチェックする
    {
        _cells[x, y].Shown = !_cells[x, y].Shown; // 表返りフラグを立てる
        Refresh(); // 画面再描画
    }
}

if文を1個はさんでいますが、これはわずかながらですが4行目のカードのさらに下の部分をクリックしたりすることもできるためです。
この場合、Yが4になってしまったりしてそのまま処理しようとすると IndexOutOfRangeException になってしまうので、その対策です。
(0始まりなので、0~3でないといけないです。)

Form1_MouseClick はフォームデザイナでフォームのMouseClickイベントに紐づけるか、
コンストラクタに次のように書いておきます。

MouseClick += Form1_MouseClick; // 【追加2】マウスクリック紐づけ

実行してクリックしてみましょう。

めくれるようになりましたね!

★ ★ ★

でもこれ、いくらでもめくれちゃいますね。

そう、まだ判定と、よかったとき除去する処理、だめだったとき裏返す処理が未実装です。

あと、見た目が地味なのでもう少しいい感じにしたいですね。
そういえば、冒頭で「色」とか言っていましたね。

これらはまた、次回お伝えします!

今日はここまで。それでは!